From 94986e745141118cace0391da1b4dc8533408751 Mon Sep 17 00:00:00 2001 From: Tor Norbye Date: Tue, 9 Aug 2011 15:58:34 -0700 Subject: Action API improvements This changeset changes the Actions mechanism for view rules to add support for the following: * Delayed computation of submenu contents. Before this, a view rule would have to produce the full tree of actions to be shown in menus and submenus - for example including all the properties, and in turn all the enumerated values for those properties and so on. Now there's a Provider interface which can be used to compute these menu items only when the menu is actually opened. The properties menu now takes advantage of this. This was also necessary to implement the following new feature: * The layout editor context menu now also lists not just the properties for the currently selected views, but also the properties for the parents. For example, if you open the context menu, you'll see the properties for the button you just right clicked on, but there will also be a "frameLayout1" submenu containing the actions for the parent of the button, and a "linearLayout1" submenu for the parent linear layout. This is useful when a parent layout doesn't have blank space on its own so it is difficult to target. A future CL will use the lazy initialization to add more options to the properties menu. * Support for arbitrary nesting. Submenus can contain submenus can contain other submenus etc. * Custom ordering. This changeset moves the "sort priority" concept (which was already used for layout actions) up to all actions, which makes it easier for rules to cooperate on ordering because instead of appending or prepending to the superclass' context menu result, actions can now just be initialized with a sorting priority value which makes it trivial to interleave actions regardless of who adds them. This also makes it a lot easier to use custom ordering in choice menus where the ordering used to be alphabetically sorting on keys. * Improved support for multiselection. The callback interface now takes a list of nodes to apply the callback to, and actions can indicate whether they support multiple nodes. This makes it possible for actions to more directly support the case where you apply an action to multiple nodes. As before, the available actions in the context menu is limited such that it only shows the actions common to all. But now those actions can do something specific. For example, if you select "Edit Text..." on many nodes, you will get the input-string dialog once, and then the value is applied to all. Similarly, if you select "Edit Id..." it will ask for a separate id for each value (and you can cancel out of this loop). There are various API changes too. Since the Choices action (which had a map-based set of values) was removed, the OrderedChoices is now renamed Choices. The Actions subclass of MenuAction which all actions also extended has simply been moved up to the top level MenuAction. And MenuAction has been renamed to RuleActions since they are used not just for menus but for toolbars etc and the key thing about this interface is that they are intended for use by rules. Change-Id: If49f75213f2041ebfef7e84254d70d219bb766ab --- .../com/android/ide/common/api/IMenuCallback.java | 17 +- .../src/com/android/ide/common/api/IViewRule.java | 23 +- .../src/com/android/ide/common/api/MenuAction.java | 669 -------------------- .../src/com/android/ide/common/api/RuleAction.java | 656 ++++++++++++++++++++ .../android/ide/common/layout/BaseLayoutRule.java | 47 +- .../android/ide/common/layout/BaseViewRule.java | 418 ++++++++----- .../android/ide/common/layout/EditTextRule.java | 13 +- .../android/ide/common/layout/FrameLayoutRule.java | 6 +- .../android/ide/common/layout/GridLayoutRule.java | 50 +- .../ide/common/layout/LinearLayoutRule.java | 70 ++- .../com/android/ide/common/layout/MergeRule.java | 5 +- .../ide/common/layout/PropertyCallback.java | 25 +- .../ide/common/layout/RelativeLayoutRule.java | 29 +- .../android/ide/common/layout/TableLayoutRule.java | 31 +- .../android/ide/common/layout/TableRowRule.java | 4 +- .../editors/layout/gle2/DynamicContextMenu.java | 678 ++++++++++----------- .../editors/layout/gle2/GestureManager.java | 18 - .../editors/layout/gle2/LayoutActionBar.java | 70 ++- .../internal/editors/layout/gle2/LayoutCanvas.java | 5 +- .../editors/layout/gle2/PaletteControl.java | 2 +- .../internal/editors/layout/gre/RulesEngine.java | 28 +- 21 files changed, 1464 insertions(+), 1400 deletions(-) delete mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/MenuAction.java create mode 100755 eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/RuleAction.java (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src') diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IMenuCallback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IMenuCallback.java index 1906436..80f77b8 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IMenuCallback.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IMenuCallback.java @@ -16,9 +16,13 @@ package com.android.ide.common.api; +import java.util.List; + /** - * Callback interface for {@link MenuAction}s. The callback performs the actual - * work of the menu. + * Callback interface for a {@link RuleAction}. The callback performs the actual + * work of the action, and this level of indirection allows multiple actions (which + * typically do not have their own class, only their own instances) to share a single + * implementation callback class. *

* NOTE: This is not a public or final API; if you rely on this be prepared * to adjust your code for the next tools release. @@ -26,14 +30,15 @@ package com.android.ide.common.api; */ public interface IMenuCallback { /** - * Performs the actual work promised by the {@link MenuAction}. - * - * @param action The MenuAction being applied. + * Performs the actual work promised by the {@link RuleAction}. + * @param action The action being applied. + * @param selectedNodes The nodes to apply the action to * @param valueId For a Choices action, the string id of the selected choice * @param newValue 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. */ - void action(MenuAction menuAction, String valueId, Boolean newValue); + void action(RuleAction action, List selectedNodes, String valueId, + Boolean newValue); } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java index a16db28..d29ef71 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/IViewRule.java @@ -67,19 +67,22 @@ public interface IViewRule { * If null is returned, the GLE will automatically shorten the class name using its * own heuristic, which is to keep the first 2 package components and the class name. * The class name is the fqcn argument that was given - * to {@link #onInitialize(String)}. + * to {@link #onInitialize(String,IClientRulesEngine)}. * * @return Null for the default behavior or a shortened string. */ String getDisplayName(); /** - * Invoked by the Rules Engine to retrieve a set of actions to customize + * Invoked by the Rules Engine to produce a set of actions to customize * the context menu displayed for this view. The result is not cached and the * method is invoked every time the context menu is about to be shown. + *

+ * The order of the menu items is determined by the sort priority set on + * the actions. *

- * Most rules should consider returning super.getContextMenu(node) - * and appending their own custom menu actions, if any. + * Most rules should consider calling super.{@link #addContextMenuActions(List, INode)} + * as well. *

* 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 @@ -87,16 +90,18 @@ public interface IViewRule { * 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. + * @param actions a list of actions to add new context menu actions into. The order + * of the actions in this list is not important; it will be sorted by + * {@link RuleAction#getSortPriority()} later. + * @param node the node to add actions for. */ - List getContextMenu(INode node); + void addContextMenuActions(List actions, INode node); /** * Invoked by the Rules Engine to ask the parent layout for the set of layout actions * to display in the layout bar. The layout rule should add these into the provided * list. The order the items are added in does not matter; the - * {@link MenuAction#getSortPriority()} values will be used to sort the actions prior + * {@link RuleAction#getSortPriority()} values will be used to sort the actions prior * to display, which makes it easier for parent rules and deriving rules to interleave * their respective actions. * @@ -104,7 +109,7 @@ public interface IViewRule { * @param parentNode the parent of the selection, or the selection itself if the root * @param targets the targeted/selected nodes, if any */ - void addLayoutActions(List actions, + void addLayoutActions(List actions, INode parentNode, List targets); // ==== Selection ==== diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/MenuAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/MenuAction.java deleted file mode 100755 index 3e912f8..0000000 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/MenuAction.java +++ /dev/null @@ -1,669 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Eclipse Public License, Version 1.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.eclipse.org/org/documents/epl-v10.php - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.ide.common.api; - -import com.android.annotations.Nullable; - -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * A menu action represents one item in the context menu displayed by the GLE canvas. - *

- * 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. - *

- * When the canvas has a multiple selection, only actions that are present in all - * 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.
- * This allows the canvas to only display compatible actions that will work on all selected - * elements. - *

- * 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. - *

- * 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. - *

- * The {@link MenuAction} is abstract. Users should instantiate either {@link Toggle}, - * {@link Choices} or {@link Group} instead. These classes are immutable. - *

- * NOTE: This is not a public or final API; if you rely on this be prepared - * to adjust your code for the next tools release. - *

- */ -public abstract class MenuAction implements Comparable { - - /** - * The unique id of the action. - * @see #getId() - */ - private final String mId; - /** - * The UI-visible title of the action. - */ - private final String mTitle; - - /** A URL pointing to an icon, or null */ - private URL mIconUrl; - - /** - * The sorting priority of this item; actions can be sorted according to these - */ - protected int mSortPriority; - - // Factories - - public static MenuAction createSeparator() { - return new Separator(sNextSortPriority++); - } - - public static MenuAction createSeparator(int sortPriority) { - return new Separator(sortPriority); - } - - - public static MenuAction createAction(String id, String title, String groupId, - IMenuCallback callback) { - MenuAction.Action action = new MenuAction.Action(id, title, groupId, callback); - action.setSortPriority(sNextSortPriority++); - return action; - } - - public static MenuAction createAction(String id, String title, String groupId, - IMenuCallback callback, URL iconUrl, int sortPriority) { - MenuAction action = new MenuAction.Action(id, title, groupId, callback); - action.setIconUrl(iconUrl); - action.setSortPriority(sortPriority); - return action; - } - - public static MenuAction createToggle(String id, String title, boolean isChecked, - IMenuCallback callback) { - Toggle action = new Toggle(id, title, isChecked, callback); - action.setSortPriority(sNextSortPriority++); - return action; - } - - public static MenuAction createToggle(String id, String title, boolean isChecked, - IMenuCallback callback, URL iconUrl, int sortPriority) { - Toggle toggle = new Toggle(id, title, isChecked, callback); - toggle.setIconUrl(iconUrl); - toggle.setSortPriority(sortPriority); - return toggle; - } - - public static MenuAction createChoices(String id, String title, String groupId, - IMenuCallback callback, List titles, List iconUrls, List ids, - String current) { - OrderedChoices action = new OrderedChoices(id, title, groupId, callback, titles, iconUrls, - ids, current); - action.setSortPriority(sNextSortPriority++); - return action; - } - - public static OrderedChoices createChoices(String id, String title, String groupId, - IMenuCallback callback, List titles, List iconUrls, List ids, - String current, URL iconUrl, int sortPriority) { - OrderedChoices choices = new OrderedChoices(id, title, groupId, callback, titles, iconUrls, - ids, current); - choices.setIconUrl(iconUrl); - choices.setSortPriority(sortPriority); - return choices; - } - - public static OrderedChoices createChoices(String id, String title, String groupId, - IMenuCallback callback, ChoiceProvider provider, - String current, URL iconUrl, int sortPriority) { - OrderedChoices choices = new DelayedOrderedChoices(id, title, groupId, callback, - current, provider); - choices.setIconUrl(iconUrl); - choices.setSortPriority(sortPriority); - return choices; - } - - /** - * 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; - } - - /** - * Gets a URL pointing to an icon to use for this action, if any. - * - * @return a URL pointing to an icon to use for this action, or null - */ - public URL getIconUrl() { - return mIconUrl; - } - - /** - * Sets a URL pointing to an icon to use for this action, if any. - * - * @param iconUrl a URL pointing to an icon to use for this action, or null - */ - public void setIconUrl(URL iconUrl) { - mIconUrl = iconUrl; - } - - /** - * Sets a priority used for sorting this action - * - * @param sortPriority a priority used for sorting this action - */ - public void setSortPriority(int sortPriority) { - mSortPriority = sortPriority; - } - - private static int sNextSortPriority = 0; - - /** - * Return a priority used for sorting this action - * - * @return a priority used for sorting this action - */ - public int getSortPriority() { - return mSortPriority; - } - - // Implements Comparable - public int compareTo(MenuAction other) { - if (mSortPriority != other.mSortPriority) { - return mSortPriority - other.mSortPriority; - } - - return mTitle.compareTo(other.mTitle); - } - - /** - * A group of actions, displayed in a sub-menu. - *

- * 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. - *

- * The current canvas has the following implementation details:
- * - 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.
- * - 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.
- * - Actions which group-id do not match any known group will simply be placed in the - * root context menu.
- * - Empty groups do not create visible sub-menus.
- * 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 class Action extends MenuAction { - - /** - * A callback executed when the action is selected in the context menu. - */ - private final IMenuCallback mCallback; - - /** - * An optional group id, to place the action in a given sub-menu. - * @null This value can be null. - */ - @Nullable - private final String mGroupId; - - /** - * Constructs a new base {@link MenuAction} with its ID, title and action callback. - * - * @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 callback The callback executed when the action is selected. - * Must not be null. - */ - public Action(String id, String title, String groupId, IMenuCallback callback) { - super(id, title); - mGroupId = groupId; - mCallback = callback; - } - - /** - * Returns the callback executed when the action is selected in the - * context menu. Cannot be null. - */ - public IMenuCallback getCallback() { - return mCallback; - } - - /** - * Returns the optional id of an existing group or null - * @null This value can be null. - */ - @Nullable - 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 separator to display between actions */ - public static class Separator extends MenuAction { - /** Construct using the factory {@link #createSeparator(int)} */ - private Separator(int sortPriority) { - super("_separator", ""); //$NON-NLS-1$ //$NON-NLS-2$ - mSortPriority = sortPriority; - } - } - - /** - * 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. - *

- * Two toggles are equal if they have the same id, title and group-id. - * It is expected for the checked state and action callback to be different. - */ - public static class Toggle extends Action { - /** - * True if the item is displayed with a check mark. - */ - private final 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 callback A callback to execute when the context menu item is - * selected. - */ - public Toggle(String id, String title, boolean isChecked, IMenuCallback callback) { - this(id, title, isChecked, null /* group-id */, callback); - } - - /** - * 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 callback A callback to execute when the context menu item is - * selected. - */ - public Toggle(String id, String title, boolean isChecked, String groupId, - IMenuCallback callback) { - super(id, title, groupId, callback); - 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 callback 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(); - } - } - - /** - * Like {@link Choices}, but with an explicit ordering among the children, and with - * optional icons on each child choice - */ - public static class OrderedChoices extends Action { - protected List mTitles; - protected List mIconUrls; - protected List mIds; - private boolean mRadio; - - /** - * 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. - */ - protected final String mCurrent; - - public OrderedChoices(String id, String title, String groupId, IMenuCallback callback, - List titles, List iconUrls, List ids, String current) { - super(id, title, groupId, callback); - mTitles = titles; - mIconUrls = iconUrls; - mIds = ids; - mCurrent = current; - } - - public List getIconUrls() { - return mIconUrls; - } - - public List getIds() { - return mIds; - } - - public List getTitles() { - return mTitles; - } - - public String getCurrent() { - return mCurrent; - } - - /** - * Set whether this choice list is best visualized as a radio group (instead of a - * dropdown) - * - * @param radio true if this choice list should be visualized as a radio group - */ - public void setRadio(boolean radio) { - mRadio = radio; - } - - /** - * Returns true if this choice list is best visualized as a radio group (instead - * of a dropdown) - * - * @return true if this choice list should be visualized as a radio group - */ - public boolean isRadio() { - return mRadio; - } - } - - /** Provides the set of choices associated with an {@link OrderedChoices} object - when they are needed. Useful for lazy initialization of context menus and popup menus - until they are actually needed. */ - public interface ChoiceProvider { - /** - * Adds in the needed titles, iconUrls (if any) and ids. - * - * @param titles a list of titles that the provider should append to - * @param iconUrls a list of icon URLs that the provider should append to - * @param ids a list of ids that the provider should append to - */ - public void addChoices(List titles, List iconUrls, List ids); - } - - /** Like {@link OrderedChoices}, but the set of choices is computed lazily */ - private static class DelayedOrderedChoices extends OrderedChoices { - private final ChoiceProvider mProvider; - - public DelayedOrderedChoices(String id, String title, String groupId, - IMenuCallback callback, String current, ChoiceProvider provider) { - super(id, title, groupId, callback, null, null, null, current); - mProvider = provider; - } - - private void ensureInitialized() { - if (mTitles == null) { - mTitles = new ArrayList(); - mIconUrls = new ArrayList(); - mIds = new ArrayList(); - - mProvider.addChoices(mTitles, mIconUrls, mIds); - } - } - - @Override - public List getIconUrls() { - ensureInitialized(); - return mIconUrls; - } - - @Override - public List getIds() { - ensureInitialized(); - return mIds; - } - - @Override - public List getTitles() { - ensureInitialized(); - return mTitles; - } - } - - /** - * 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. - *

- * Implementation detail: empty choices will not be displayed in the context menu. - *

- * Choice items are sorted by their id, using String's natural sorting order. - *

- * Two multiple choices are equal if they have the same id, title, group-id and choices. - * It is expected for the current state and action callback to be different. - */ - 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 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 callback A callback to execute when the context menu item is - * selected. - */ - public Choices(String id, String title, - Map choices, - String current, - IMenuCallback callback) { - this(id, title, choices, current, null /* group-id */, callback); - } - - /** - * 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 callback A callback to execute when the context menu item is - * selected. - */ - public Choices(String id, String title, - Map choices, - String current, - String groupId, - IMenuCallback callback) { - super(id, title, groupId, callback); - mChoices = choices; - mCurrent = current; - } - - /** - * Return the map of id=>choice-title. The map could be empty but not null. - */ - public Map 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 callback 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/common/api/RuleAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/RuleAction.java new file mode 100755 index 0000000..2ebab36 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/api/RuleAction.java @@ -0,0 +1,656 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.common.api; + +import com.android.util.Pair; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * A {@link RuleAction} represents an action provided by an {@link IViewRule}, typically + * shown in a context menu or in the layout actions bar. + *

+ * Each action should have a reasonably unique ID. This is used when multiple nodes + * are selected to filter the actions down to just those actions that are supported + * across all selected nodes. If an action does not support multiple nodes, it can + * return false from {@link #supportsMultipleNodes()}. + *

+ * Actions can be grouped into a hierarchy of sub-menus using the {@link NestedAction} class, + * or into a flat submenu using the {@link Choices} class. + *

+ * Actions (including separators) all have a "sort priority", and this is used to + * sort the menu items or toolbar buttons into a specific order. + *

+ * NOTE: This is not a public or final API; if you rely on this be prepared + * to adjust your code for the next tools release. + *

+ */ +public class RuleAction implements Comparable { + /** + * Character used to split multiple checked choices. + * The pipe character "|" is used, to natively match Android resource flag separators. + */ + public final static String CHOICE_SEP = "|"; //$NON-NLS-1$ + + /** + * Same as {@link #CHOICE_SEP} but safe for use in regular expressions. + */ + public final static String CHOICE_SEP_PATTERN = Pattern.quote(CHOICE_SEP); + + /** + * The unique id of the action. + * @see #getId() + */ + private final String mId; + /** + * The UI-visible title of the action. + */ + private final String mTitle; + + /** A URL pointing to an icon, or null */ + private URL mIconUrl; + + /** + * A callback executed when the action is selected in the context menu. + */ + private final IMenuCallback mCallback; + + /** + * The sorting priority of this item; actions can be sorted according to these + */ + protected final int mSortPriority; + + /** + * Whether this action supports multiple nodes, see + * {@link #supportsMultipleNodes()} for details. + */ + private final boolean mSupportsMultipleNodes; + + /** + * Special value which will insert a separator in the choices' submenu. + */ + public final static String SEPARATOR = "----"; + + // Factories + + /** + * Constructs a new separator which will be shown in places where separators + * are supported such as context menus + * + * @param sortPriority a priority used for sorting this action + * @return a new separator + */ + public static Separator createSeparator(int sortPriority) { + return new Separator(sortPriority, true /* supportsMultipleNodes*/); + } + + /** + * Constructs a new base {@link RuleAction} with its ID, title and action callback. + * + * @param id The unique ID of the action. Must not be null. + * @param title The title of the action. Must not be null. + * @param callback The callback executed when the action is selected. + * Must not be null. + * @param iconUrl a URL pointing to an icon to use for this action, or null + * @param sortPriority a priority used for sorting this action + * @param supportsMultipleNodes whether this action supports multiple nodes, + * see {@link #supportsMultipleNodes()} for details + * @return the new {@link RuleAction} + */ + public static RuleAction createAction(String id, String title, + IMenuCallback callback, URL iconUrl, int sortPriority, boolean supportsMultipleNodes) { + RuleAction action = new RuleAction(id, title, callback, sortPriority, + supportsMultipleNodes); + action.setIconUrl(iconUrl); + + return 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 callback A callback to execute when the context menu item is + * selected. + * @param iconUrl a URL pointing to an icon to use for this action, or null + * @param sortPriority a priority used for sorting this action + * @param supportsMultipleNodes whether this action supports multiple nodes, + * see {@link #supportsMultipleNodes()} for details + * @return the new {@link Toggle} + */ + public static Toggle createToggle(String id, String title, boolean isChecked, + IMenuCallback callback, URL iconUrl, int sortPriority, + boolean supportsMultipleNodes) { + Toggle toggle = new Toggle(id, title, isChecked, callback, sortPriority, + supportsMultipleNodes); + toggle.setIconUrl(iconUrl); + return toggle; + } + + /** + * Creates a new immutable multiple-choice action with a defined ordered set + * of action children. + * + * @param id The unique id of the action. Cannot be null. + * @param title The title of the action to be displayed to the user + * @param provider Provides the actions to be shown as children of this + * action + * @param callback A callback to execute when the context menu item is + * selected. + * @param iconUrl the icon to use for the multiple choice action itself + * @param sortPriority the sorting priority to use for the multiple choice + * action itself + * @param supportsMultipleNodes whether this action supports multiple nodes, + * see {@link #supportsMultipleNodes()} for details + * @return the new {@link NestedAction} + */ + public static NestedAction createChoices(String id, String title, + IMenuCallback callback, URL iconUrl, + int sortPriority, boolean supportsMultipleNodes, ActionProvider provider) { + NestedAction choices = new NestedAction(id, title, provider, callback, + sortPriority, supportsMultipleNodes); + choices.setIconUrl(iconUrl); + return choices; + } + + /** + * Creates a new immutable multiple-choice action with a defined ordered set + * of children. + * + * @param id The unique id of the action. Cannot be null. + * @param title The title of the action to be displayed to the user + * @param iconUrls The icon urls for the children items (may be null) + * @param ids The internal ids for the children + * @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 callback A callback to execute when the context menu item is + * selected. + * @param titles The UI-visible titles of the children + * @param iconUrl the icon to use for the multiple choice action itself + * @param sortPriority the sorting priority to use for the multiple choice + * action itself + * @param supportsMultipleNodes whether this action supports multiple nodes, + * see {@link #supportsMultipleNodes()} for details + * @return the new {@link Choices} + */ + public static Choices createChoices(String id, String title, + IMenuCallback callback, List titles, List iconUrls, List ids, + String current, URL iconUrl, int sortPriority, boolean supportsMultipleNodes) { + Choices choices = new Choices(id, title, callback, titles, iconUrls, + ids, current, sortPriority, supportsMultipleNodes); + choices.setIconUrl(iconUrl); + + return choices; + } + + /** + * Creates a new immutable multiple-choice action with a defined ordered set + * of children. + * + * @param id The unique id of the action. Cannot be null. + * @param title The title of the action to be displayed to the user + * @param iconUrls The icon urls for the children items (may 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 callback A callback to execute when the context menu item is + * selected. + * @param iconUrl the icon to use for the multiple choice action itself + * @param sortPriority the sorting priority to use for the multiple choice + * action itself + * @param supportsMultipleNodes whether this action supports multiple nodes, + * see {@link #supportsMultipleNodes()} for details + * @param idsAndTitles a list of pairs (of ids and titles) to use for the + * menu items + * @return the new {@link Choices} + */ + public static Choices createChoices(String id, String title, + IMenuCallback callback, List iconUrls, + String current, URL iconUrl, int sortPriority, + boolean supportsMultipleNodes, List> idsAndTitles) { + int itemCount = idsAndTitles.size(); + List titles = new ArrayList(itemCount); + List ids = new ArrayList(itemCount); + for (Pair pair : idsAndTitles) { + ids.add(pair.getFirst()); + titles.add(pair.getSecond()); + } + Choices choices = new Choices(id, title, callback, titles, iconUrls, + ids, current, sortPriority, supportsMultipleNodes); + choices.setIconUrl(iconUrl); + return choices; + } + + /** + * Creates a new immutable multiple-choice action with lazily computed children. + * + * @param id The unique id of the action. Cannot be null. + * @param title The title of the multiple-choice itself + * @param callback A callback to execute when the context menu item is + * selected. + * @param provider the provider which provides choices lazily + * @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 choice + * alternatives. There can be more than one id separated by + * {@link #CHOICE_SEP}. + * @param iconUrl the icon to use for the multiple choice action itself + * @param sortPriority the sorting priority to use for the multiple choice + * action itself + * @param supportsMultipleNodes whether this action supports multiple nodes, + * see {@link #supportsMultipleNodes()} for details + * @return the new {@link Choices} + */ + public static Choices createChoices(String id, String title, + IMenuCallback callback, ChoiceProvider provider, + String current, URL iconUrl, int sortPriority, boolean supportsMultipleNodes) { + Choices choices = new DelayedChoices(id, title, callback, + current, provider, sortPriority, supportsMultipleNodes); + choices.setIconUrl(iconUrl); + return choices; + } + + /** + * Creates a new {@link RuleAction} 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. + * @param callback A callback to execute when the context menu item is + * selected. + * @param sortPriority a priority used for sorting this action + * @param supportsMultipleNodes the new return value for + * {@link #supportsMultipleNodes()} + */ + private RuleAction(String id, String title, IMenuCallback callback, int sortPriority, + boolean supportsMultipleNodes) { + mId = id; + mTitle = title; + mSortPriority = sortPriority; + mSupportsMultipleNodes = supportsMultipleNodes; + mCallback = callback; + } + + /** + * 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. + * + * @return the unique id of the action, never null + */ + public String getId() { + return mId; + } + + /** + * Returns the UI-visible title of the action, shown in the context menu. + * Cannot be null. + * + * @return the user name of the action, never 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 RuleAction) { + RuleAction rhs = (RuleAction) 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; + } + + /** + * Whether this action supports multiple nodes. An action which supports + * multiple nodes can be applied to different nodes by passing in different + * nodes to its callback. Some actions are hardcoded for a specific node (typically + * one that isn't selected, such as an action which affects the parent of a selected + * node), and these actions will not be added to the context menu when more than + * one node is selected. + * + * @return true if this node supports multiple nodes + */ + public boolean supportsMultipleNodes() { + return mSupportsMultipleNodes; + } + + /** + * 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; + } + + /** + * Gets a URL pointing to an icon to use for this action, if any. + * + * @return a URL pointing to an icon to use for this action, or null + */ + public URL getIconUrl() { + return mIconUrl; + } + + /** + * Sets a URL pointing to an icon to use for this action, if any. + * + * @param iconUrl a URL pointing to an icon to use for this action, or null + * @return this action, to allow setter chaining + */ + public RuleAction setIconUrl(URL iconUrl) { + mIconUrl = iconUrl; + + return this; + } + + /** + * Return a priority used for sorting this action + * + * @return a priority used for sorting this action + */ + public int getSortPriority() { + return mSortPriority; + } + + /** + * Returns the callback executed when the action is selected in the + * context menu. Cannot be null. + * + * @return the callback, never null + */ + public IMenuCallback getCallback() { + return mCallback; + } + + // Implements Comparable + public int compareTo(RuleAction other) { + if (mSortPriority != other.mSortPriority) { + return mSortPriority - other.mSortPriority; + } + + return mTitle.compareTo(other.mTitle); + } + + @Override + public String toString() { + return "RuleAction [id=" + mId + ", title=" + mTitle + ", priority=" + mSortPriority + "]"; + } + + /** A separator to display between actions */ + public static class Separator extends RuleAction { + /** Construct using the factory {@link #createSeparator(int)} */ + private Separator(int sortPriority, boolean supportsMultipleNodes) { + super("_separator", "", null, sortPriority, //$NON-NLS-1$ //$NON-NLS-2$ + supportsMultipleNodes); + } + } + + /** + * 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. + *

+ * Two toggles are equal if they have the same id, title and group-id. + * It is expected for the checked state and action callback to be different. + */ + public static class Toggle extends RuleAction { + /** + * True if the item is displayed with a check mark. + */ + private final boolean mIsChecked; + + /** + * 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 callback A callback to execute when the context menu item is + * selected. + */ + private Toggle(String id, String title, boolean isChecked, + IMenuCallback callback, int sortPriority, boolean supportsMultipleNodes) { + super(id, title, callback, sortPriority, supportsMultipleNodes); + mIsChecked = isChecked; + } + + /** + * Returns true if the item is displayed with a check mark. + * + * @return 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 and title. + * It is acceptable for the checked state and action callback 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 and title. + */ + @Override + public int hashCode() { + return super.hashCode(); + } + } + + /** + * An ordered list of choices the user can choose between. For choosing between + * actions, there is a {@link NestedAction} class. + */ + public static class Choices extends RuleAction { + protected List mTitles; + protected List mIconUrls; + protected List mIds; + private boolean mRadio; + + /** + * 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. + */ + protected final String mCurrent; + + private Choices(String id, String title, IMenuCallback callback, + List titles, List iconUrls, List ids, String current, + int sortPriority, boolean supportsMultipleNodes) { + super(id, title, callback, sortPriority, supportsMultipleNodes); + mTitles = titles; + mIconUrls = iconUrls; + mIds = ids; + mCurrent = current; + } + + /** + * Returns the list of urls to icons to display for each choice, or null + * + * @return the list of urls to icons to display for each choice, or null + */ + public List getIconUrls() { + return mIconUrls; + } + + /** + * Returns the list of ids for the menu choices, never null + * + * @return the list of ids for the menu choices, never null + */ + public List getIds() { + return mIds; + } + + /** + * Returns the titles to be displayed for the menu choices, never null + * + * @return the titles to be displayed for the menu choices, never null + */ + public List getTitles() { + return mTitles; + } + + /** + * Returns the current value of the choice + * + * @return the current value of the choice, possibly null + */ + public String getCurrent() { + return mCurrent; + } + + /** + * Set whether this choice list is best visualized as a radio group (instead of a + * dropdown) + * + * @param radio true if this choice list should be visualized as a radio group + */ + public void setRadio(boolean radio) { + mRadio = radio; + } + + /** + * Returns true if this choice list is best visualized as a radio group (instead + * of a dropdown) + * + * @return true if this choice list should be visualized as a radio group + */ + public boolean isRadio() { + return mRadio; + } + } + + /** + * An ordered list of actions the user can choose between. Similar to + * {@link Choices} but for actions instead. + */ + public static class NestedAction extends RuleAction { + /** The provider to produce the list of nested actions when needed */ + private final ActionProvider mProvider; + + private NestedAction(String id, String title, ActionProvider provider, + IMenuCallback callback, int sortPriority, + boolean supportsMultipleNodes) { + super(id, title, callback, sortPriority, supportsMultipleNodes); + mProvider = provider; + } + + /** + * Returns the nested actions available for the given node + * + * @param node the node to look up nested actions for + * @return a list of nested actions + */ + public List getNestedActions(INode node) { + return mProvider.getNestedActions(node); + } + } + + /** Like {@link Choices}, but the set of choices is computed lazily */ + private static class DelayedChoices extends Choices { + private final ChoiceProvider mProvider; + + private DelayedChoices(String id, String title, + IMenuCallback callback, String current, ChoiceProvider provider, + int sortPriority, boolean supportsMultipleNodes) { + super(id, title, callback, null, null, null, current, sortPriority, + supportsMultipleNodes); + mProvider = provider; + } + + private void ensureInitialized() { + if (mTitles == null) { + mTitles = new ArrayList(); + mIconUrls = new ArrayList(); + mIds = new ArrayList(); + + mProvider.addChoices(mTitles, mIconUrls, mIds); + } + } + + @Override + public List getIconUrls() { + ensureInitialized(); + return mIconUrls; + } + + @Override + public List getIds() { + ensureInitialized(); + return mIds; + } + + @Override + public List getTitles() { + ensureInitialized(); + return mTitles; + } + } + + /** + * Provides the set of nested action choices associated with a {@link NestedAction} + * object when they are needed. Useful for lazy initialization of context + * menus and popup menus until they are actually needed. + */ + public interface ActionProvider { + /** + * Returns the nested actions available for the given node + * + * @param node the node to look up nested actions for + * @return a list of nested actions + */ + public List getNestedActions(INode node); + } + + /** + * Provides the set of choices associated with an {@link Choices} + * object when they are needed. Useful for lazy initialization of context + * menus and popup menus until they are actually needed. + */ + public interface ChoiceProvider { + /** + * Adds in the needed titles, iconUrls (if any) and ids. + * Use {@link RuleAction#SEPARATOR} to create separators. + * + * @param titles a list of titles that the provider should append to + * @param iconUrls a list of icon URLs that the provider should append to + * @param ids a list of ids that the provider should append to + */ + public void addChoices(List titles, List iconUrls, List ids); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java index 1a99385..5191d25 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseLayoutRule.java @@ -66,8 +66,9 @@ import com.android.ide.common.api.IGraphics; import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; import com.android.ide.common.api.INodeHandler; -import com.android.ide.common.api.MenuAction; -import com.android.ide.common.api.MenuAction.ChoiceProvider; +import com.android.ide.common.api.IViewRule; +import com.android.ide.common.api.RuleAction; +import com.android.ide.common.api.RuleAction.ChoiceProvider; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import com.android.ide.common.api.Segment; @@ -85,6 +86,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +/** + * A {@link IViewRule} for all layouts. + */ public class BaseLayoutRule extends BaseViewRule { private static final String ACTION_FILL_WIDTH = "_fillW"; //$NON-NLS-1$ private static final String ACTION_FILL_HEIGHT = "_fillH"; //$NON-NLS-1$ @@ -102,7 +106,7 @@ public class BaseLayoutRule extends BaseViewRule { // The Margin layout parameters are available for LinearLayout, FrameLayout, RelativeLayout, // and their subclasses. - protected final MenuAction createMarginAction(final INode parentNode, + protected final RuleAction createMarginAction(final INode parentNode, final List children) { final List targets = children == null || children.size() == 0 ? @@ -111,7 +115,8 @@ public class BaseLayoutRule extends BaseViewRule { final INode first = targets.get(0); IMenuCallback actionCallback = new IMenuCallback() { - public void action(MenuAction action, final String valueId, final Boolean newValue) { + public void action(RuleAction action, List selectedNodes, + final String valueId, final Boolean newValue) { parentNode.editXml("Change Margins", new INodeHandler() { public void handle(INode n) { String uri = ANDROID_URI; @@ -137,13 +142,13 @@ public class BaseLayoutRule extends BaseViewRule { } }; - return MenuAction.createAction(ACTION_MARGIN, "Change Margins...", null, actionCallback, - ICON_MARGINS, 40); + return RuleAction.createAction(ACTION_MARGIN, "Change Margins...", actionCallback, + ICON_MARGINS, 40, false); } // Both LinearLayout and RelativeLayout have a gravity (but RelativeLayout applies it // to the parent whereas for LinearLayout it's on the children) - protected final MenuAction createGravityAction(final List targets, final + protected final RuleAction createGravityAction(final List targets, final String attributeName) { if (targets != null && targets.size() > 0) { final INode first = targets.get(0); @@ -162,20 +167,19 @@ public class BaseLayoutRule extends BaseViewRule { } }; - return MenuAction.createChoices("_gravity", "Change Gravity", //$NON-NLS-1$ - null, + return RuleAction.createChoices("_gravity", "Change Gravity", //$NON-NLS-1$ new PropertyCallback(targets, "Change Gravity", ANDROID_URI, attributeName), provider, first.getStringAttr(ANDROID_URI, attributeName), ICON_GRAVITY, - 43); + 43, false); } return null; } @Override - public void addLayoutActions(List actions, final INode parentNode, + public void addLayoutActions(List actions, final INode parentNode, final List children) { super.addLayoutActions(actions, parentNode, children); @@ -186,7 +190,8 @@ public class BaseLayoutRule extends BaseViewRule { // Shared action callback IMenuCallback actionCallback = new IMenuCallback() { - public void action(MenuAction action, final String valueId, final Boolean newValue) { + public void action(RuleAction action, List selectedNodes, + final String valueId, final Boolean newValue) { final String actionId = action.getId(); final String undoLabel; if (actionId.equals(ACTION_FILL_WIDTH)) { @@ -218,10 +223,10 @@ public class BaseLayoutRule extends BaseViewRule { } }; - actions.add(MenuAction.createToggle(ACTION_FILL_WIDTH, "Toggle Fill Width", - isFilled(first, ATTR_LAYOUT_WIDTH), actionCallback, ICON_FILL_WIDTH, 10)); - actions.add(MenuAction.createToggle(ACTION_FILL_HEIGHT, "Toggle Fill Height", - isFilled(first, ATTR_LAYOUT_HEIGHT), actionCallback, ICON_FILL_HEIGHT, 20)); + actions.add(RuleAction.createToggle(ACTION_FILL_WIDTH, "Toggle Fill Width", + isFilled(first, ATTR_LAYOUT_WIDTH), actionCallback, ICON_FILL_WIDTH, 10, false)); + actions.add(RuleAction.createToggle(ACTION_FILL_HEIGHT, "Toggle Fill Height", + isFilled(first, ATTR_LAYOUT_HEIGHT), actionCallback, ICON_FILL_HEIGHT, 20, false)); } // ==== Paste support ==== @@ -256,6 +261,10 @@ public class BaseLayoutRule extends BaseViewRule { * This method is invoked by BaseView when onPaste() is called -- * views don't generally accept children and instead use the target node as * a hint to paste "before" it. + * + * @param parentNode the parent node we're pasting into + * @param targetNode the first selected node + * @param elements the elements being pasted */ public void onPasteBeforeChild(INode parentNode, INode targetNode, IDragElement[] elements) { @@ -737,6 +746,12 @@ public class BaseLayoutRule extends BaseViewRule { } } + /** + * Returns the maximum number of pixels will be considered a "match" when snapping + * resize or move positions to edges or other constraints + * + * @return the maximum number of pixels to consider for snapping + */ public static final int getMaxMatchDistance() { // TODO - make constant once we're happy with the feel return 20; diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java index 625ae34..66688d9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java @@ -35,22 +35,29 @@ import com.android.ide.common.api.IDragElement; import com.android.ide.common.api.IGraphics; import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; -import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IValidator; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; -import com.android.ide.common.api.MenuAction; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; +import com.android.ide.common.api.RuleAction; +import com.android.ide.common.api.RuleAction.ActionProvider; +import com.android.ide.common.api.RuleAction.ChoiceProvider; +import com.android.ide.common.api.RuleAction.Choices; import com.android.ide.common.api.SegmentType; +import com.android.util.Pair; +import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; /** @@ -58,16 +65,15 @@ import java.util.Set; */ public class BaseViewRule implements IViewRule { // Strings used as internal ids, group ids and prefixes for actions - private static final String FALSE_ID = "2f"; //$NON-NLS-1$ - private static final String TRUE_ID = "1t"; //$NON-NLS-1$ + private static final String FALSE_ID = "false"; //$NON-NLS-1$ + private static final String TRUE_ID = "true"; //$NON-NLS-1$ private static final String PROP_PREFIX = "@prop@"; //$NON-NLS-1$ - private static final String SEPARATOR_ID = "~1sep"; //$NON-NLS-1$ - private static final String DEFAULT_ID = "~2clr"; //$NON-NLS-1$ + private static final String CLEAR_ID = "clear"; //$NON-NLS-1$ private static final String PROPERTIES_ID = "properties"; //$NON-NLS-1$ private static final String EDIT_TEXT_ID = "edittext"; //$NON-NLS-1$ private static final String EDIT_ID_ID = "editid"; //$NON-NLS-1$ - private static final String WIDTH_ID = "layout_1width"; //$NON-NLS-1$ - private static final String HEIGHT_ID = "layout_2height"; //$NON-NLS-1$ + private static final String WIDTH_ID = "layout_width"; //$NON-NLS-1$ + private static final String HEIGHT_ID = "layout_height"; //$NON-NLS-1$ private static final String ZCUSTOM = "zcustom"; //$NON-NLS-1$ protected IClientRulesEngine mRulesEngine; @@ -115,108 +121,114 @@ public class BaseViewRule implements IViewRule { * - Explicit layout_width and layout_height attributes. * - List of all other simple toggle attributes. */ - public List getContextMenu(final INode selectedNode) { - // Compute the key for mAttributesMap. This depends on the type of this - // node and its parent in the view hierarchy. - StringBuilder keySb = new StringBuilder(); - keySb.append(selectedNode.getFqcn()); - keySb.append('_'); - INode parent = selectedNode.getParent(); - if (parent != null) { - keySb.append(parent.getFqcn()); - } - final String key = keySb.toString(); - - String custom_w = null; - String curr_w = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH); + public void addContextMenuActions(List actions, final INode selectedNode) { + String width = null; + String currentWidth = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH); String fillParent = getFillParentValueName(); boolean canMatchParent = supportsMatchParent(); - if (canMatchParent && VALUE_FILL_PARENT.equals(curr_w)) { - curr_w = VALUE_MATCH_PARENT; - } else if (!canMatchParent && VALUE_MATCH_PARENT.equals(curr_w)) { - curr_w = VALUE_FILL_PARENT; - } else if (!VALUE_WRAP_CONTENT.equals(curr_w) && !fillParent.equals(curr_w)) { - custom_w = curr_w; + if (canMatchParent && VALUE_FILL_PARENT.equals(currentWidth)) { + currentWidth = VALUE_MATCH_PARENT; + } else if (!canMatchParent && VALUE_MATCH_PARENT.equals(currentWidth)) { + currentWidth = VALUE_FILL_PARENT; + } else if (!VALUE_WRAP_CONTENT.equals(currentWidth) && !fillParent.equals(currentWidth)) { + width = currentWidth; } - String custom_h = null; - String curr_h = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT); + String height = null; + String currentHeight = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT); - if (canMatchParent && VALUE_FILL_PARENT.equals(curr_h)) { - curr_h = VALUE_MATCH_PARENT; - } else if (!canMatchParent && VALUE_MATCH_PARENT.equals(curr_h)) { - curr_h = VALUE_FILL_PARENT; - } else if (!VALUE_WRAP_CONTENT.equals(curr_h) && !fillParent.equals(curr_h)) { - custom_h = curr_h; + if (canMatchParent && VALUE_FILL_PARENT.equals(currentHeight)) { + currentHeight = VALUE_MATCH_PARENT; + } else if (!canMatchParent && VALUE_MATCH_PARENT.equals(currentHeight)) { + currentHeight = VALUE_FILL_PARENT; + } else if (!VALUE_WRAP_CONTENT.equals(currentHeight) + && !fillParent.equals(currentHeight)) { + height = currentHeight; } - final String customWidth = custom_w; - final String customHeight = custom_h; - - IMenuCallback onChange = new IMenuCallback() { + final String newWidth = width; + final String newHeight = height; + final IMenuCallback onChange = new IMenuCallback() { public void action( - final MenuAction action, - final String valueId, - final Boolean newValue) { + final RuleAction action, + final List selectedNodes, + final String valueId, final Boolean newValue) { String fullActionId = action.getId(); boolean isProp = fullActionId.startsWith(PROP_PREFIX); final String actionId = isProp ? fullActionId.substring(PROP_PREFIX.length()) : fullActionId; - final INode node = selectedNode; if (fullActionId.equals(WIDTH_ID)) { - final String newAttrValue = getValue(valueId, customWidth); + final String newAttrValue = getValue(valueId, newWidth); if (newAttrValue != null) { - node.editXml("Change Attribute " + ATTR_LAYOUT_WIDTH, - new PropertySettingNodeHandler(ANDROID_URI, - ATTR_LAYOUT_WIDTH, newAttrValue)); + for (INode node : selectedNodes) { + node.editXml("Change Attribute " + ATTR_LAYOUT_WIDTH, + new PropertySettingNodeHandler(ANDROID_URI, + ATTR_LAYOUT_WIDTH, newAttrValue)); + } } return; } else if (fullActionId.equals(HEIGHT_ID)) { // Ask the user - final String newAttrValue = getValue(valueId, customHeight); + final String newAttrValue = getValue(valueId, newHeight); if (newAttrValue != null) { - node.editXml("Change Attribute " + ATTR_LAYOUT_HEIGHT, - new PropertySettingNodeHandler(ANDROID_URI, - ATTR_LAYOUT_HEIGHT, newAttrValue)); + for (INode node : selectedNodes) { + node.editXml("Change Attribute " + ATTR_LAYOUT_HEIGHT, + new PropertySettingNodeHandler(ANDROID_URI, + ATTR_LAYOUT_HEIGHT, newAttrValue)); + } } return; } else if (fullActionId.equals(EDIT_ID_ID)) { - // Strip off the @id prefix stuff - String oldId = node.getStringAttr(ANDROID_URI, ATTR_ID); - oldId = stripIdPrefix(ensureValidString(oldId)); - IValidator validator = mRulesEngine.getResourceValidator(); - String newId = mRulesEngine.displayInput("New Id:", oldId, validator); - if (newId != null && newId.trim().length() > 0) { - if (!newId.startsWith(NEW_ID_PREFIX)) { - newId = NEW_ID_PREFIX + stripIdPrefix(newId); + // Ids must be set individually so open the id dialog for each + // selected node (though allow cancel to break the loop) + for (INode node : selectedNodes) { + // Strip off the @id prefix stuff + String oldId = node.getStringAttr(ANDROID_URI, ATTR_ID); + oldId = stripIdPrefix(ensureValidString(oldId)); + IValidator validator = mRulesEngine.getResourceValidator(); + String newId = mRulesEngine.displayInput("New Id:", oldId, validator); + if (newId != null && newId.trim().length() > 0) { + if (!newId.startsWith(NEW_ID_PREFIX)) { + newId = NEW_ID_PREFIX + stripIdPrefix(newId); + } + node.editXml("Change ID", new PropertySettingNodeHandler(ANDROID_URI, + ATTR_ID, newId)); + } else if (newId == null) { + // Cancelled + break; } - node.editXml("Change ID", new PropertySettingNodeHandler(ANDROID_URI, - ATTR_ID, newId)); - } - } else if (fullActionId.equals(EDIT_TEXT_ID)) { - String oldText = node.getStringAttr(ANDROID_URI, ATTR_TEXT); - oldText = ensureValidString(oldText); - String newText = mRulesEngine.displayResourceInput("string", oldText); //$NON-NLS-1$ - if (newText != null) { - node.editXml("Change Text", new PropertySettingNodeHandler(ANDROID_URI, - ATTR_TEXT, newText.length() > 0 ? newText : null)); } - } - - if (isProp) { - Map props = mAttributesMap.get(key); - final Prop prop = (props != null) ? props.get(actionId) : null; - - if (prop != null) { - // For custom values (requiring an input dialog) input the - // value outside the undo-block - final String customValue = prop.isStringEdit() - ? inputAttributeValue(node, actionId) : null; - - node.editXml("Change Attribute " + actionId, new INodeHandler() { - public void handle(INode n) { + return; + } else { + INode firstNode = selectedNodes.get(0); + if (fullActionId.equals(EDIT_TEXT_ID)) { + String oldText = selectedNodes.size() == 1 + ? firstNode.getStringAttr(ANDROID_URI, ATTR_TEXT) + : ""; //$NON-NLS-1$ + oldText = ensureValidString(oldText); + String newText = mRulesEngine.displayResourceInput("string", oldText); //$NON-NLS-1$ + if (newText != null) { + for (INode node : selectedNodes) { + node.editXml("Change Text", + new PropertySettingNodeHandler(ANDROID_URI, + ATTR_TEXT, newText.length() > 0 ? newText : null)); + } + } + return; + } else if (isProp) { + String key = getPropertyMapKey(selectedNode); + Map props = mAttributesMap.get(key); + final Prop prop = (props != null) ? props.get(actionId) : null; + + if (prop != null) { + // For custom values (requiring an input dialog) input the + // value outside the undo-block + final String customValue = prop.isStringEdit() + ? inputAttributeValue(firstNode, actionId) : null; + + for (INode n : selectedNodes) { if (prop.isToggle()) { // case of toggle String value = ""; //$NON-NLS-1$ @@ -229,7 +241,7 @@ public class BaseViewRule implements IViewRule { } else if (prop.isFlag()) { // case of a flag String values = ""; //$NON-NLS-1$ - if (!valueId.equals(DEFAULT_ID)) { + if (!valueId.equals(CLEAR_ID)) { values = n.getStringAttr(ANDROID_URI, actionId); Set newValues = new HashSet(); if (values != null) { @@ -247,7 +259,7 @@ public class BaseViewRule implements IViewRule { } else if (prop.isEnum()) { // case of an enum String value = ""; //$NON-NLS-1$ - if (!valueId.equals(DEFAULT_ID)) { + if (!valueId.equals(CLEAR_ID)) { value = newValue ? valueId : ""; //$NON-NLS-1$ } n.setAttribute(ANDROID_URI, actionId, value); @@ -259,7 +271,7 @@ public class BaseViewRule implements IViewRule { } } } - }); + } } } } @@ -318,40 +330,91 @@ public class BaseViewRule implements IViewRule { } }; - MenuAction.Action editText = null; IAttributeInfo textAttribute = selectedNode.getAttributeInfo(ANDROID_URI, ATTR_TEXT); if (textAttribute != null) { - editText = new MenuAction.Action(EDIT_TEXT_ID, "Edit Text...", null, onChange); + actions.add(RuleAction.createAction(EDIT_TEXT_ID, "Edit Text...", onChange, + null, 10, true)); } - List list1 = Arrays.asList(new MenuAction[] { - editText, // could be null - will be ignored by menu creation code - new MenuAction.Action(EDIT_ID_ID, "Edit ID...", null, onChange), - - new MenuAction.Choices(WIDTH_ID, "Layout Width", - mapify( - VALUE_WRAP_CONTENT, "Wrap Content", - canMatchParent ? VALUE_MATCH_PARENT : VALUE_FILL_PARENT, - canMatchParent ? "Match Parent" : "Fill Parent", - custom_w, custom_w, - ZCUSTOM, "Other..." - ), - curr_w, - onChange ), - new MenuAction.Choices(HEIGHT_ID, "Layout Height", - mapify( - VALUE_WRAP_CONTENT, "Wrap Content", - canMatchParent ? VALUE_MATCH_PARENT : VALUE_FILL_PARENT, - canMatchParent ? "Match Parent" : "Fill Parent", - custom_h, custom_h, - ZCUSTOM, "Other..." - ), - curr_h, - onChange ), - new MenuAction.Group(PROPERTIES_ID, "Properties") + actions.add(RuleAction.createAction(EDIT_ID_ID, "Edit ID...", onChange, null, 20, true)); + + // Create width choice submenu + List> widthChoices = new ArrayList>(4); + widthChoices.add(Pair.of(VALUE_WRAP_CONTENT, "Wrap Content")); + if (canMatchParent) { + widthChoices.add(Pair.of(VALUE_MATCH_PARENT, "Match Parent")); + } else { + widthChoices.add(Pair.of(VALUE_FILL_PARENT, "Fill Parent")); + } + if (width != null) { + widthChoices.add(Pair.of(width, width)); + } + widthChoices.add(Pair.of(ZCUSTOM, "Other...")); + actions.add(RuleAction.createChoices( + WIDTH_ID, "Layout Width", + onChange, + null /* iconUrls */, + currentWidth, + null, 30, + true, // supportsMultipleNodes + widthChoices)); + + // Create height choice submenu + List> heightChoices = new ArrayList>(4); + heightChoices.add(Pair.of(VALUE_WRAP_CONTENT, "Wrap Content")); + if (canMatchParent) { + heightChoices.add(Pair.of(VALUE_MATCH_PARENT, "Match Parent")); + } else { + heightChoices.add(Pair.of(VALUE_FILL_PARENT, "Fill Parent")); + } + if (height != null) { + heightChoices.add(Pair.of(height, height)); + } + heightChoices.add(Pair.of(ZCUSTOM, "Other...")); + actions.add(RuleAction.createChoices( + HEIGHT_ID, "Layout Height", + onChange, + null /* iconUrls */, + currentHeight, + null, 40, + true, + heightChoices)); + + actions.add(RuleAction.createSeparator(45)); + RuleAction properties = RuleAction.createChoices(PROPERTIES_ID, "Properties", + onChange /*callback*/, null /*icon*/, 50, + true /*supportsMultipleNodes*/, new ActionProvider() { + public List getNestedActions(INode node) { + List propertyActions = createPropertyActions(node, + getPropertyMapKey(node), onChange); + + return propertyActions; + } }); - // Prepare a list of all simple properties. + actions.add(properties); + } + + private static String getPropertyMapKey(INode node) { + // Compute the key for mAttributesMap. This depends on the type of this + // node and its parent in the view hierarchy. + StringBuilder sb = new StringBuilder(); + sb.append(node.getFqcn()); + sb.append('_'); + INode parent = node.getParent(); + if (parent != null) { + sb.append(parent.getFqcn()); + } + return sb.toString(); + } + + /** + * Creates a list of nested actions representing the property-setting + * actions for the given selected node + */ + private List createPropertyActions(final INode selectedNode, final String key, + final IMenuCallback onChange) { + List propertyActions = new ArrayList(); Map props = mAttributesMap.get(key); if (props == null) { @@ -399,12 +462,10 @@ public class BaseViewRule implements IViewRule { mAttributesMap.put(key, props); } - List list2 = new ArrayList(); - + int nextPriority = 10; for (Map.Entry entry : props.entrySet()) { String id = entry.getKey(); Prop p = entry.getValue(); - MenuAction a = null; if (p.isToggle()) { // Toggles are handled as a multiple-choice between true, false // and nothing (clear) @@ -416,51 +477,91 @@ public class BaseViewRule implements IViewRule { } else if ("false".equals(value)) { //$NON-NLS-1$ value = FALSE_ID; } else { - value = "4clr"; //$NON-NLS-1$ + value = CLEAR_ID; } - - a = new MenuAction.Choices( - PROP_PREFIX + id, - p.getTitle(), - mapify( - TRUE_ID, "True", - FALSE_ID, "False", - "3sep", MenuAction.Choices.SEPARATOR, //$NON-NLS-1$ - "4clr", "Default"), //$NON-NLS-1$ - value, - PROPERTIES_ID, - onChange); + Choices action = RuleAction.createChoices(PROP_PREFIX + id, p.getTitle(), + onChange, BOOLEAN_CHOICE_PROVIDER, + value, + null, nextPriority++, + true); + propertyActions.add(action); } else if (p.getChoices() != null) { // Enum or flags. Their possible values are the multiple-choice // items, with an extra "clear" option to remove everything. String current = selectedNode.getStringAttr(ANDROID_URI, id); if (current == null || current.length() == 0) { - current = DEFAULT_ID; + current = CLEAR_ID; } - a = new MenuAction.Choices( - PROP_PREFIX + id, - p.getTitle(), - concatenate( - p.getChoices(), - mapify( - SEPARATOR_ID, MenuAction.Choices.SEPARATOR, - DEFAULT_ID, "Default" - ) - ), - current, - PROPERTIES_ID, - onChange); + Choices action = RuleAction.createChoices(PROP_PREFIX + id, p.getTitle(), + onChange, new EnumPropertyChoiceProvider(p), + current, + null, nextPriority++, + true); + propertyActions.add(action); } else { - a = new MenuAction.Action( + RuleAction action = RuleAction.createAction( PROP_PREFIX + id, p.getTitle(), - PROPERTIES_ID, - onChange); + onChange, + null, nextPriority++, + true); + propertyActions.add(action); + } + } + + // The properties are coming out of map key order which isn't right + Collections.sort(propertyActions, new Comparator() { + public int compare(RuleAction action1, RuleAction action2) { + return action1.getTitle().compareTo(action2.getTitle()); } - list2.add(a); + }); + return propertyActions; + } + + /** + * A {@link ChoiceProvder} which provides alternatives suitable for choosing + * values for a boolean property: true, false, or "default". + */ + private static ChoiceProvider BOOLEAN_CHOICE_PROVIDER = new ChoiceProvider() { + public void addChoices(List titles, List iconUrls, List ids) { + titles.add("True"); + ids.add(TRUE_ID); + + titles.add("False"); + ids.add(FALSE_ID); + + titles.add(RuleAction.SEPARATOR); + ids.add(RuleAction.SEPARATOR); + + titles.add("Default"); + ids.add(CLEAR_ID); + } + }; + + /** + * A {@link ChoiceProvider} which provides the various available + * attribute values available for a given {@link Prop} property descriptor. + */ + private static class EnumPropertyChoiceProvider implements ChoiceProvider { + private Prop mProperty; + + public EnumPropertyChoiceProvider(Prop property) { + super(); + this.mProperty = property; } - return concatenate(list1, list2); + public void addChoices(List titles, List iconUrls, List ids) { + for (Entry entry : mProperty.getChoices().entrySet()) { + ids.add(entry.getKey()); + titles.add(entry.getValue()); + } + + titles.add(RuleAction.SEPARATOR); + ids.add(RuleAction.SEPARATOR); + + titles.add("Default"); + ids.add(CLEAR_ID); + } } /** @@ -504,21 +605,6 @@ public class BaseViewRule implements IViewRule { return sb.toString(); } - // Concatenate two menu action lists. Move these utilities into MenuAction - static List concatenate(List pre, List post) { - List result = new ArrayList(pre.size() + post.size()); - result.addAll(pre); - result.addAll(post); - return result; - } - - static List concatenate(List pre, MenuAction post) { - List result = new ArrayList(pre.size() + 1); - result.addAll(pre); - result.add(post); - return result; - } - static Map concatenate(Map pre, Map post) { Map result = new HashMap(pre.size() + post.size()); result.putAll(pre); @@ -555,7 +641,7 @@ public class BaseViewRule implements IViewRule { return null; } - public void addLayoutActions(List actions, INode parentNode, + public void addLayoutActions(List actions, INode parentNode, List children) { } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java index dfe9d6f..e26df79 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/EditTextRule.java @@ -23,7 +23,7 @@ import com.android.ide.common.api.INode; import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; -import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.RuleAction; import java.util.List; @@ -51,12 +51,15 @@ public class EditTextRule extends BaseViewRule { * Adds a "Request Focus" menu item. */ @Override - public List getContextMenu(final INode selectedNode) { + public void addContextMenuActions(List actions, final INode selectedNode) { + super.addContextMenuActions(actions, selectedNode); + final boolean hasFocus = hasFocus(selectedNode); final String label = hasFocus ? "Clear Focus" : "Request Focus"; IMenuCallback onChange = new IMenuCallback() { - public void action(MenuAction menuAction, String valueId, Boolean newValue) { + public void action(RuleAction menuAction, List selectedNodes, + String valueId, Boolean newValue) { selectedNode.editXml(label, new INodeHandler() { public void handle(INode node) { INode focus = findFocus(findRoot(node)); @@ -71,8 +74,8 @@ public class EditTextRule extends BaseViewRule { } }; - return concatenate(super.getContextMenu(selectedNode), - new MenuAction.Action("_setfocus", label, null, onChange)); //$NON-NLS-1$ + actions.add(RuleAction.createAction("_setfocus", label, onChange, //$NON-NLS-1$ + null, 5, false /*supportsMultipleNodes*/)); } /** Returns true if the given node currently has focus */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FrameLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FrameLayoutRule.java index b8c7408..884034f 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FrameLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/FrameLayoutRule.java @@ -31,7 +31,7 @@ import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IViewMetadata; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; -import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.RuleAction; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import com.android.ide.common.api.IViewMetadata.FillPreference; @@ -156,10 +156,10 @@ public class FrameLayoutRule extends BaseLayoutRule { } @Override - public void addLayoutActions(List actions, final INode parentNode, + public void addLayoutActions(List actions, final INode parentNode, final List children) { super.addLayoutActions(actions, parentNode, children); - actions.add(MenuAction.createSeparator(25)); + actions.add(RuleAction.createSeparator(25)); actions.add(createMarginAction(parentNode, children)); if (children != null && children.size() > 0) { actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY)); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java index 2e28713..47ea3d9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/GridLayoutRule.java @@ -31,8 +31,8 @@ import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IViewRule; -import com.android.ide.common.api.MenuAction; -import com.android.ide.common.api.MenuAction.OrderedChoices; +import com.android.ide.common.api.RuleAction; +import com.android.ide.common.api.RuleAction.Choices; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import com.android.ide.common.api.SegmentType; @@ -130,32 +130,32 @@ public class GridLayoutRule extends BaseLayoutRule { } @Override - public void addLayoutActions(List actions, final INode parentNode, + public void addLayoutActions(List actions, final INode parentNode, final List children) { super.addLayoutActions(actions, parentNode, children); - OrderedChoices orientationAction = MenuAction.createChoices( + Choices orientationAction = RuleAction.createChoices( ACTION_ORIENTATION, "Orientation", //$NON-NLS-1$ - null, new PropertyCallback(Collections.singletonList(parentNode), + new PropertyCallback(Collections.singletonList(parentNode), "Change LinearLayout Orientation", ANDROID_URI, ATTR_ORIENTATION), Arrays . asList("Set Horizontal Orientation", "Set Vertical Orientation"), Arrays. asList(ICON_HORIZONTAL, ICON_VERTICAL), Arrays. asList( "horizontal", "vertical"), getCurrentOrientation(parentNode), - null /* icon */, -10); + null /* icon */, -10, false); orientationAction.setRadio(true); actions.add(orientationAction); // Gravity and margins if (children != null && children.size() > 0) { - actions.add(MenuAction.createSeparator(35)); + actions.add(RuleAction.createSeparator(35)); actions.add(createMarginAction(parentNode, children)); actions.add(createGravityAction(children, ATTR_LAYOUT_GRAVITY)); } IMenuCallback actionCallback = new IMenuCallback() { - public void action(final MenuAction action, final String valueId, - final Boolean newValue) { + public void action(final RuleAction action, List selectedNodes, + final String valueId, final Boolean newValue) { parentNode.editXml("Add/Remove Row/Column", new INodeHandler() { public void handle(INode n) { String id = action.getId(); @@ -194,33 +194,33 @@ public class GridLayoutRule extends BaseLayoutRule { }; // Add Row and Add Column - actions.add(MenuAction.createSeparator(150)); - actions.add(MenuAction.createAction(ACTION_ADD_COL, "Add Column", null, actionCallback, - ICON_ADD_COL, 160)); - actions.add(MenuAction.createAction(ACTION_ADD_ROW, "Add Row", null, actionCallback, - ICON_ADD_ROW, 165)); + actions.add(RuleAction.createSeparator(150)); + actions.add(RuleAction.createAction(ACTION_ADD_COL, "Add Column", actionCallback, + ICON_ADD_COL, 160, false /* supportsMultipleNodes */)); + actions.add(RuleAction.createAction(ACTION_ADD_ROW, "Add Row", actionCallback, + ICON_ADD_ROW, 165, false)); // Remove Row and Remove Column (if something is selected) if (children != null && children.size() > 0) { // TODO: Add "Merge Columns" and "Merge Rows" ? - actions.add(MenuAction.createAction(ACTION_REMOVE_COL, "Remove Column", null, - actionCallback, ICON_REMOVE_COL, 170)); - actions.add(MenuAction.createAction(ACTION_REMOVE_ROW, "Remove Row", null, - actionCallback, ICON_REMOVE_ROW, 175)); + actions.add(RuleAction.createAction(ACTION_REMOVE_COL, "Remove Column", + actionCallback, ICON_REMOVE_COL, 170, false)); + actions.add(RuleAction.createAction(ACTION_REMOVE_ROW, "Remove Row", + actionCallback, ICON_REMOVE_ROW, 175, false)); } - actions.add(MenuAction.createSeparator(185)); + actions.add(RuleAction.createSeparator(185)); - actions.add(MenuAction.createToggle(ACTION_SNAP, "Snap to Grid", - sSnapToGrid, actionCallback, ICON_SNAP, 190)); + actions.add(RuleAction.createToggle(ACTION_SNAP, "Snap to Grid", + sSnapToGrid, actionCallback, ICON_SNAP, 190, false)); - actions.add(MenuAction.createToggle(ACTION_SHOW_GRID, "Show Structure", - sShowStructure, actionCallback, ICON_SHOW_GRID, 200)); + actions.add(RuleAction.createToggle(ACTION_SHOW_GRID, "Show Structure", + sShowStructure, actionCallback, ICON_SHOW_GRID, 200, false)); // Temporary: Diagnostics for GridLayout - actions.add(MenuAction.createToggle(ACTION_DEBUG, "Debug", - sDebugGridLayout, actionCallback, null, 210)); + actions.add(RuleAction.createToggle(ACTION_DEBUG, "Debug", + sDebugGridLayout, actionCallback, null, 210, false)); } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java index bf1efb5..ae42fc3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/LinearLayoutRule.java @@ -44,13 +44,14 @@ import com.android.ide.common.api.IViewMetadata; import com.android.ide.common.api.IViewMetadata.FillPreference; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; -import com.android.ide.common.api.MenuAction; -import com.android.ide.common.api.MenuAction.OrderedChoices; +import com.android.ide.common.api.RuleAction; +import com.android.ide.common.api.RuleAction.Choices; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import com.android.ide.common.api.SegmentType; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.sdklib.SdkConstants; +import com.android.util.Pair; import java.net.URL; import java.util.ArrayList; @@ -91,21 +92,26 @@ public class LinearLayoutRule extends BaseLayoutRule { * Add an explicit Orientation toggle to the context menu. */ @Override - public List getContextMenu(final INode selectedNode) { + public void addContextMenuActions(List actions, final INode selectedNode) { + super.addContextMenuActions(actions, selectedNode); if (supportsOrientation()) { String current = getCurrentOrientation(selectedNode); - IMenuCallback onChange = new PropertyCallback(Collections.singletonList(selectedNode), + IMenuCallback onChange = new PropertyCallback( + null, // use passed in nodes instead to support multiple nodes "Change LinearLayout Orientation", ANDROID_URI, ATTR_ORIENTATION); - return concatenate(super.getContextMenu(selectedNode), - new MenuAction.Choices(ACTION_ORIENTATION, "Orientation", //$NON-NLS-1$ - mapify( - "horizontal", "Horizontal", //$NON-NLS-1$ - "vertical", "Vertical" //$NON-NLS-1$ - ), - current, onChange)); - } else { - return super.getContextMenu(selectedNode); + List> alternatives = new ArrayList>(2); + alternatives.add(Pair.of("horizontal", "Horizontal")); //$NON-NLS-1$ + alternatives.add(Pair.of("vertical", "Vertical")); //$NON-NLS-1$ + RuleAction action = RuleAction.createChoices( + ACTION_ORIENTATION, "Orientation", //$NON-NLS-1$ + onChange, + null /* iconUrls */, + current, + null /* icon */, 5, true, + alternatives); + + actions.add(action); } } @@ -145,22 +151,22 @@ public class LinearLayoutRule extends BaseLayoutRule { } @Override - public void addLayoutActions(List actions, final INode parentNode, + public void addLayoutActions(List actions, final INode parentNode, final List children) { super.addLayoutActions(actions, parentNode, children); if (supportsOrientation()) { - OrderedChoices action = MenuAction.createChoices( + Choices action = RuleAction.createChoices( ACTION_ORIENTATION, "Orientation", //$NON-NLS-1$ - null, new PropertyCallback(Collections.singletonList(parentNode), "Change LinearLayout Orientation", ANDROID_URI, ATTR_ORIENTATION), - Arrays.asList("Set Horizontal Orientation", "Set Vertical Orientation"), + Arrays.asList("Set Horizontal Orientation","Set Vertical Orientation"), Arrays.asList(ICON_HORIZONTAL, ICON_VERTICAL), Arrays.asList("horizontal", "vertical"), getCurrentOrientation(parentNode), null /* icon */, - -10 + -10, + false /* supportsMultipleNodes */ ); action.setRadio(true); actions.add(action); @@ -168,17 +174,17 @@ public class LinearLayoutRule extends BaseLayoutRule { if (!isVertical(parentNode)) { String current = parentNode.getStringAttr(ANDROID_URI, ATTR_BASELINE_ALIGNED); boolean isAligned = current == null || Boolean.valueOf(current); - actions.add(MenuAction.createToggle(null, "Toggle Baseline Alignment", + actions.add(RuleAction.createToggle(null, "Toggle Baseline Alignment", isAligned, new PropertyCallback(Collections.singletonList(parentNode), "Change Baseline Alignment", ANDROID_URI, ATTR_BASELINE_ALIGNED), // TODO: Also set index? - ICON_BASELINE, 38)); + ICON_BASELINE, 38, false)); } // Gravity if (children != null && children.size() > 0) { - actions.add(MenuAction.createSeparator(35)); + actions.add(RuleAction.createSeparator(35)); // Margins actions.add(createMarginAction(parentNode, children)); @@ -188,8 +194,8 @@ public class LinearLayoutRule extends BaseLayoutRule { // Weights IMenuCallback actionCallback = new IMenuCallback() { - public void action(final MenuAction action, final String valueId, - final Boolean newValue) { + public void action(final RuleAction action, List selectedNodes, + final String valueId, final Boolean newValue) { parentNode.editXml("Change Weight", new INodeHandler() { public void handle(INode n) { String id = action.getId(); @@ -222,15 +228,15 @@ public class LinearLayoutRule extends BaseLayoutRule { }); } }; - actions.add(MenuAction.createSeparator(50)); - actions.add(MenuAction.createAction(ACTION_DISTRIBUTE, "Distribute Weights Evenly", - null, actionCallback, ICON_DISTRIBUTE, 60)); - actions.add(MenuAction.createAction(ACTION_DOMINATE, "Assign All Weight", - null, actionCallback, ICON_DOMINATE, 70)); - actions.add(MenuAction.createAction(ACTION_WEIGHT, "Change Layout Weight", null, - actionCallback, ICON_WEIGHTS, 80)); - actions.add(MenuAction.createAction(ACTION_CLEAR, "Clear All Weights", - null, actionCallback, ICON_CLEAR_WEIGHTS, 90)); + actions.add(RuleAction.createSeparator(50)); + actions.add(RuleAction.createAction(ACTION_DISTRIBUTE, "Distribute Weights Evenly", + actionCallback, ICON_DISTRIBUTE, 60, false /*supportsMultipleNodes*/)); + actions.add(RuleAction.createAction(ACTION_DOMINATE, "Assign All Weight", + actionCallback, ICON_DOMINATE, 70, false)); + actions.add(RuleAction.createAction(ACTION_WEIGHT, "Change Layout Weight", + actionCallback, ICON_WEIGHTS, 80, false)); + actions.add(RuleAction.createAction(ACTION_CLEAR, "Clear All Weights", + actionCallback, ICON_CLEAR_WEIGHTS, 90, false)); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MergeRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MergeRule.java index 77f5c22..12358f9 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MergeRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/MergeRule.java @@ -17,7 +17,7 @@ package com.android.ide.common.layout; import com.android.ide.common.api.INode; -import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.RuleAction; import java.util.List; @@ -29,9 +29,8 @@ public class MergeRule extends FrameLayoutRule { // on top of each other at (0,0) @Override - public List getContextMenu(INode selectedNode) { + public void addContextMenuActions(List actions, final INode selectedNode) { // Deliberately ignore super.getContextMenu(); we don't want to attempt to list // properties for the tag - return null; } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertyCallback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertyCallback.java index 559aac8..45cd2c5 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertyCallback.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/PropertyCallback.java @@ -19,7 +19,7 @@ package com.android.ide.common.layout; import com.android.ide.common.api.IMenuCallback; import com.android.ide.common.api.INode; import com.android.ide.common.api.INodeHandler; -import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.RuleAction; import java.util.List; @@ -33,6 +33,16 @@ public class PropertyCallback implements IMenuCallback { private final String mUri; private final String mAttribute; + /** + * Creates a new property callback. + * + * @param targetNodes the nodes to apply the property to, or null to use the + * nodes pass into the + * {@link #action(RuleAction, List, String, Boolean)} method. + * @param undoLabel the label to use for the undo action + * @param uri the attribute URI to apply + * @param attribute the attribute name to apply + */ public PropertyCallback(List targetNodes, String undoLabel, String uri, String attribute) { super(); @@ -43,13 +53,18 @@ public class PropertyCallback implements IMenuCallback { } // ---- Implements IMenuCallback ---- - public void action(MenuAction action, final String valueId, final Boolean newValue) { - if (mTargetNodes == null || mTargetNodes.size() == 0) { + public void action(RuleAction action, List selectedNodes, + final String valueId, final Boolean newValue) { + if (mTargetNodes != null && mTargetNodes.size() > 0) { + selectedNodes = mTargetNodes; + } + if (selectedNodes == null || selectedNodes.size() == 0) { return; } - mTargetNodes.get(0).editXml(mUndoLabel, new INodeHandler() { + final List nodes = selectedNodes; + selectedNodes.get(0).editXml(mUndoLabel, new INodeHandler() { public void handle(INode n) { - for (INode targetNode : mTargetNodes) { + for (INode targetNode : nodes) { if (valueId != null) { targetNode.setAttribute(mUri, mAttribute, valueId); } else { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java index d53436f..90952c9 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/RelativeLayoutRule.java @@ -50,7 +50,7 @@ import com.android.ide.common.api.INode.IAttribute; import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; -import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.RuleAction; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import com.android.ide.common.api.SegmentType; @@ -315,17 +315,18 @@ public class RelativeLayoutRule extends BaseLayoutRule { // ==== Layout Actions Bar ==== @Override - public void addLayoutActions(List actions, final INode parentNode, + public void addLayoutActions(List actions, final INode parentNode, final List children) { super.addLayoutActions(actions, parentNode, children); actions.add(createGravityAction(Collections.singletonList(parentNode), ATTR_GRAVITY)); - actions.add(MenuAction.createSeparator(25)); + actions.add(RuleAction.createSeparator(25)); actions.add(createMarginAction(parentNode, children)); IMenuCallback callback = new IMenuCallback() { - public void action(MenuAction action, final String valueId, final Boolean newValue) { + public void action(RuleAction action, List selectedNodes, + final String valueId, final Boolean newValue) { final String id = action.getId(); if (id.equals(ACTION_CENTER_VERTICAL)|| id.equals(ACTION_CENTER_HORIZONTAL)) { parentNode.editXml("Center", new INodeHandler() { @@ -356,18 +357,18 @@ public class RelativeLayoutRule extends BaseLayoutRule { // Centering actions if (children != null && children.size() > 0) { - actions.add(MenuAction.createSeparator(150)); - actions.add(MenuAction.createAction(ACTION_CENTER_VERTICAL, "Center Vertically", null, - callback, ICON_CENTER_VERTICALLY, 160)); - actions.add(MenuAction.createAction(ACTION_CENTER_HORIZONTAL, "Center Horizontally", - null, callback, ICON_CENTER_HORIZONTALLY, 170)); + actions.add(RuleAction.createSeparator(150)); + actions.add(RuleAction.createAction(ACTION_CENTER_VERTICAL, "Center Vertically", + callback, ICON_CENTER_VERTICALLY, 160, false)); + actions.add(RuleAction.createAction(ACTION_CENTER_HORIZONTAL, "Center Horizontally", + callback, ICON_CENTER_HORIZONTALLY, 170, false)); } - actions.add(MenuAction.createSeparator(80)); - actions.add(MenuAction.createToggle(ACTION_SHOW_CONSTRAINTS, "Show Constraints", - sShowConstraints, callback, ICON_SHOW_CONSTRAINTS, 180)); - actions.add(MenuAction.createToggle(ACTION_SHOW_STRUCTURE, "Show All Relationships", - sShowStructure, callback, ICON_SHOW_STRUCTURE, 190)); + actions.add(RuleAction.createSeparator(80)); + actions.add(RuleAction.createToggle(ACTION_SHOW_CONSTRAINTS, "Show Constraints", + sShowConstraints, callback, ICON_SHOW_CONSTRAINTS, 180, false)); + actions.add(RuleAction.createToggle(ACTION_SHOW_STRUCTURE, "Show All Relationships", + sShowStructure, callback, ICON_SHOW_STRUCTURE, 190, false)); } private void centerHorizontally(INode node) { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java index d556e7d..4ae31b7 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableLayoutRule.java @@ -24,7 +24,7 @@ import com.android.ide.common.api.INode; import com.android.ide.common.api.INodeHandler; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; -import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.RuleAction; import com.android.ide.common.api.SegmentType; import java.net.URL; @@ -69,21 +69,22 @@ public class TableLayoutRule extends LinearLayoutRule { * Add an explicit "Add Row" action to the context menu */ @Override - public List getContextMenu(final INode selectedNode) { + public void addContextMenuActions(List actions, final INode selectedNode) { + super.addContextMenuActions(actions, selectedNode); + IMenuCallback addTab = new IMenuCallback() { - public void action(MenuAction action, final String valueId, Boolean newValue) { + public void action(RuleAction action, List selectedNodes, + final String valueId, Boolean newValue) { final INode node = selectedNode; INode newRow = node.appendChild(FQCN_TABLE_ROW); mRulesEngine.select(Collections.singletonList(newRow)); } }; - return concatenate(super.getContextMenu(selectedNode), - new MenuAction.Action("_addrow", "Add Row", //$NON-NLS-1$ - null, addTab)); + actions.add(RuleAction.createAction("_addrow", "Add Row", addTab, null, 5, false)); //$NON-NLS-1$ } @Override - public void addLayoutActions(List actions, final INode parentNode, + public void addLayoutActions(List actions, final INode parentNode, final List children) { super.addLayoutActions(actions, parentNode, children); addTableLayoutActions(mRulesEngine, actions, parentNode, children); @@ -93,11 +94,11 @@ public class TableLayoutRule extends LinearLayoutRule { * Adds layout actions to add and remove toolbar items */ static void addTableLayoutActions(final IClientRulesEngine rulesEngine, - List actions, final INode parentNode, + List actions, final INode parentNode, final List children) { IMenuCallback actionCallback = new IMenuCallback() { - public void action(final MenuAction action, final String valueId, - final Boolean newValue) { + public void action(final RuleAction action, List selectedNodes, + final String valueId, final Boolean newValue) { parentNode.editXml("Add/Remove Table Row", new INodeHandler() { public void handle(INode n) { if (action.getId().equals(ACTION_ADD_ROW)) { @@ -155,14 +156,14 @@ public class TableLayoutRule extends LinearLayoutRule { }; // Add Row - actions.add(MenuAction.createSeparator(150)); - actions.add(MenuAction.createAction(ACTION_ADD_ROW, "Add Table Row", null, actionCallback, - ICON_ADD_ROW, 160)); + actions.add(RuleAction.createSeparator(150)); + actions.add(RuleAction.createAction(ACTION_ADD_ROW, "Add Table Row", actionCallback, + ICON_ADD_ROW, 160, false)); // Remove Row (if something is selected) if (children != null && children.size() > 0) { - actions.add(MenuAction.createAction(ACTION_REMOVE_ROW, "Remove Table Row", null, - actionCallback, ICON_REMOVE_ROW, 170)); + actions.add(RuleAction.createAction(ACTION_REMOVE_ROW, "Remove Table Row", + actionCallback, ICON_REMOVE_ROW, 170, false)); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java index 031e17b..dad71ed 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/TableRowRule.java @@ -21,7 +21,7 @@ import com.android.ide.common.api.DropFeedback; import com.android.ide.common.api.INode; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; -import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.RuleAction; import com.android.ide.common.api.SegmentType; import java.util.List; @@ -48,7 +48,7 @@ public class TableRowRule extends LinearLayoutRule { } @Override - public void addLayoutActions(List actions, final INode parentNode, + public void addLayoutActions(List actions, final INode parentNode, final List children) { super.addLayoutActions(actions, parentNode, children); diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java index e8cd418..dfc30fe 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/DynamicContextMenu.java @@ -16,6 +16,8 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; +import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; +import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; import static com.android.ide.common.layout.LayoutConstants.EXPANDABLE_LIST_VIEW; import static com.android.ide.common.layout.LayoutConstants.FQCN_GESTURE_OVERLAY_VIEW; import static com.android.ide.common.layout.LayoutConstants.GRID_VIEW; @@ -23,12 +25,15 @@ import static com.android.ide.common.layout.LayoutConstants.LIST_VIEW; import static com.android.ide.common.layout.LayoutConstants.SPINNER; import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_FRAGMENT; -import com.android.ide.common.api.IMenuCallback; +import com.android.ide.common.api.INode; import com.android.ide.common.api.IViewRule; -import com.android.ide.common.api.MenuAction; -import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.common.api.RuleAction; +import com.android.ide.common.api.RuleAction.Choices; +import com.android.ide.common.api.RuleAction.NestedAction; +import com.android.ide.common.api.RuleAction.Toggle; +import com.android.ide.common.layout.BaseViewRule; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeLayoutAction; import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.ChangeViewAction; @@ -39,20 +44,23 @@ import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElement import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ActionContributionItem; +import org.eclipse.jface.action.ContributionItem; 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; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Menu; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.regex.Pattern; +import java.util.Set; /** * Helper class that is responsible for adding and managing the dynamic menu items @@ -65,7 +73,7 @@ import java.util.regex.Pattern; * created by {@link OutlinePage}. Different root {@link MenuManager}s are populated, however * they are both linked to the current selection state of the {@link LayoutCanvas}. */ -/* package */ class DynamicContextMenu { +class DynamicContextMenu { /** The XML layout editor that contains the canvas that uses this menu. */ private final LayoutEditor mEditor; @@ -76,7 +84,6 @@ import java.util.regex.Pattern; /** The root menu manager of the context menu. */ private final MenuManager mMenuManager; - /** * Creates a new helper responsible for adding and managing the dynamic menu items * contributed by the {@link IViewRule} instances, based on the current selection @@ -125,22 +132,12 @@ import java.util.regex.Pattern; } /** - * This is invoked by menuAboutToShow on {@link #mMenuManager}. + * This method is invoked by menuAboutToShow on {@link #mMenuManager}. * All previous dynamic menu actions have been removed and this method can now insert * any new actions that depend on the current selection. */ private void populateDynamicContextMenu() { - // Map action-id => action object (one per selected view that defined it) - final TreeMap> actionsMap = - new TreeMap>(); - - // Map group-id => actions to place in this group. - TreeMap groupsMap = - new TreeMap(); - - int maxMenuSelection = collectDynamicMenuActions(actionsMap, groupsMap); - - // Now create the actual menu contributions + // Create the actual menu contributions String endId = mMenuManager.getItems()[0].getId(); Separator sep = new Separator(); @@ -148,73 +145,85 @@ import java.util.regex.Pattern; mMenuManager.insertBefore(endId, sep); endId = sep.getId(); - // First create the groups - Map menuGroups = new HashMap(); - for (MenuAction.Group group : groupsMap.values()) { - String id = group.getId(); - MenuManager submenu = new MenuManager(group.getTitle(), id); - menuGroups.put(id, submenu); - mMenuManager.insertBefore(endId, submenu); - endId = id; + List selections = mCanvas.getSelectionManager().getSelections(); + if (selections.size() == 0) { + return; + } + List nodes = new ArrayList(selections.size()); + for (SelectionItem item : selections) { + nodes.add(item.getNode()); } - boolean needGroupSep = !menuGroups.isEmpty(); + List menuItems = getMenuItems(nodes); + for (IContributionItem menuItem : menuItems) { + mMenuManager.insertBefore(endId, menuItem); + } - // Now fill in the actions - for (ArrayList 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; - } + insertTagSpecificMenus(endId); + insertVisualRefactorings(endId); + insertParentItems(endId); + } - if (!(actions.get(0) instanceof MenuAction.Action)) { - continue; - } + /** + * Returns the list of node-specific actions applicable to the given + * collection of nodes + * + * @param nodes the collection of nodes to look up actions for + * @return a list of contribution items applicable for all the nodes + */ + private List getMenuItems(List nodes) { + Map> allActions = new HashMap>(); + for (INode node : nodes) { + List actionList = getMenuActions((NodeProxy) node); + allActions.put(node, actionList); + } - // Arbitrarily select the first action, as all the actions with the same id - // should have the same constant attributes such as id and title. - final MenuAction.Action firstAction = (MenuAction.Action) actions.get(0); + Set availableIds = computeApplicableActionIds(allActions); - IContributionItem contrib = null; + // +10: Make room for separators too + List items = new ArrayList(availableIds.size() + 10); - if (firstAction instanceof MenuAction.Toggle) { - contrib = createDynamicMenuToggle((MenuAction.Toggle) firstAction, actionsMap); + // We'll use the actions returned by the first node. Even when there + // are multiple items selected, we'll use the first action, but pass + // the set of all selected nodes to that first action. Actions are required + // to work this way to facilitate multi selection and actions which apply + // to multiple nodes. + List firstSelectedActions = allActions.get(nodes.get(0)); - } else if (firstAction instanceof MenuAction.Choices) { - Map choiceMap = ((MenuAction.Choices) firstAction).getChoices(); - if (choiceMap != null && !choiceMap.isEmpty()) { - contrib = createDynamicChoices( - (MenuAction.Choices)firstAction, choiceMap, actionsMap); - } - } else { - // Must be a plain action - contrib = createDynamicAction(firstAction, actionsMap); + for (RuleAction action : firstSelectedActions) { + if (!availableIds.contains(action.getId()) + && !(action instanceof RuleAction.Separator)) { + // This action isn't supported by all selected items. + continue; } - if (contrib != null) { - MenuManager groupMenu = menuGroups.get(firstAction.getGroupId()); - if (groupMenu != null) { - groupMenu.add(contrib); - } else { - if (needGroupSep) { - needGroupSep = false; + items.add(createContributionItem(action, nodes)); + } - sep = new Separator(); - sep.setId("-dyn-gle-sep2"); //$NON-NLS-1$ - mMenuManager.insertBefore(endId, sep); - endId = sep.getId(); - } - mMenuManager.insertBefore(endId, contrib); + return items; + } + + private void insertParentItems(String endId) { + List selection = mCanvas.getSelectionManager().getSelections(); + if (selection.size() == 1) { + mMenuManager.insertBefore(endId, new Separator()); + INode parent = selection.get(0).getNode().getParent(); + while (parent != null) { + String id = parent.getStringAttr(ANDROID_URI, ATTR_ID); + String label; + if (id != null && id.length() > 0) { + label = BaseViewRule.stripIdPrefix(id); + } else { + // Use the view name, such as "Button", as the label + label = parent.getFqcn(); + // Strip off package + label = label.substring(label.lastIndexOf('.') + 1); } + mMenuManager.insertBefore(endId, new NestedParentMenu(label, parent)); + parent = parent.getParent(); } + mMenuManager.insertBefore(endId, new Separator()); } - - insertTagSpecificMenus(endId); - insertVisualRefactorings(endId); } private void insertVisualRefactorings(String endId) { @@ -264,98 +273,68 @@ import java.util.regex.Pattern; } /** - * Collects all the {@link MenuAction} contributed by the {@link IViewRule} of the - * current selection. - * This is the first step of {@link #populateDynamicContextMenu()}. + * Given a map from selection items to list of applicable actions (produced + * by {@link #computeApplicableActions()}) this method computes the set of + * common actions and returns the action ids of these actions. * - * @param outActionsMap Map that collects all the contributed actions. - * @param outGroupsMap Map that collects all the contributed groups (sub-menus). - * @return The max number of selected items that contributed the same action ID. - * This is used later to filter on multiple selections so that we can display only - * actions that are common to all selected items that contributed at least one action. + * @param actions a map from selection item to list of actions applicable to + * that selection item + * @return set of action ids for the actions that are present in the action + * lists for all selected items */ - private int collectDynamicMenuActions( - final TreeMap> outActionsMap, - final TreeMap outGroupsMap) { - int maxMenuSelection = 0; - for (SelectionItem selection : mCanvas.getSelectionManager().getSelections()) { - List 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) { - - // Allow nulls - ignore these. Make it easier to define action lists - // literals where some items may not be included (because their references - // are null). - if (action == null) { - continue; - } - - if (action.getId() == null || action.getTitle() == null) { - // TODO Log verbose error for invalid action. - continue; - } - - String id = action.getId(); - - if (action instanceof MenuAction.Group) { - if (!outGroupsMap.containsKey(id)) { - outGroupsMap.put(id, (MenuAction.Group) action); - } - continue; - } - - ArrayList actions = outActionsMap.get(id); - if (actions == null) { - actions = new ArrayList(); - outActionsMap.put(id, actions); - } - - // All the actions for the same id should have be equal - if (!actions.isEmpty()) { - if (!action.equals(actions.get(0))) { - // TODO Log verbose error for invalid type mismatch. + private Set computeApplicableActionIds(Map> actions) { + if (actions.size() > 1) { + // More than one view is selected, so we have to filter down the available + // actions such that only those actions that are defined for all the views + // are shown + Map idCounts = new HashMap(); + for (Map.Entry> entry : actions.entrySet()) { + List actionList = entry.getValue(); + for (RuleAction action : actionList) { + if (!action.supportsMultipleNodes()) { continue; } + String id = action.getId(); + if (id != null) { + assert id != null : action; + Integer count = idCounts.get(id); + if (count == null) { + idCounts.put(id, Integer.valueOf(1)); + } else { + idCounts.put(id, count + 1); + } + } } - - actions.add(action); - foundAction = true; } - - if (foundAction) { - maxMenuSelection++; + Integer selectionCount = Integer.valueOf(actions.size()); + Set validIds = new HashSet(idCounts.size()); + for (Map.Entry entry : idCounts.entrySet()) { + Integer count = entry.getValue(); + if (selectionCount.equals(count)) { + String id = entry.getKey(); + validIds.add(id); + } } + return validIds; + } else { + List actionList = actions.values().iterator().next(); + Set validIds = new HashSet(actionList.size()); + for (RuleAction action : actionList) { + String id = action.getId(); + validIds.add(id); + } + return validIds; } - return maxMenuSelection; } /** - * Returns the menu actions computed by the rule associated with this view. + * Returns the menu actions computed by the rule associated with this node. * - * @param vi the canvas view info we need menu actions for - * @return a list of {@link MenuAction} objects applicable to the view info + * @param node the canvas node we need menu actions for + * @return a list of {@link RuleAction} objects applicable to the node */ - public List getMenuActions(CanvasViewInfo vi) { - if (vi == null) { - return null; - } - - NodeProxy node = mCanvas.getNodeFactory().create(vi); - if (node == null) { - return null; - } - - List actions = mCanvas.getRulesEngine().callGetContextMenu(node); + private List getMenuActions(NodeProxy node) { + List actions = mCanvas.getRulesEngine().callGetContextMenu(node); if (actions == null || actions.size() == 0) { return null; } @@ -364,273 +343,234 @@ import java.util.regex.Pattern; } /** - * Invoked by {@link #populateDynamicContextMenu()} to create a new menu item - * for a {@link MenuAction.Toggle}. - *

- * Toggles are represented by a checked menu item. + * Creates a {@link ContributionItem} for the given {@link RuleAction}. * - * @param firstAction The toggle action to convert to a menu item. In the case of a - * multiple selection, this is the first of many similar actions. - * @param actionsMap Map of all contributed actions. - * @return a new {@link IContributionItem} to add to the context menu + * @param action the action to create a {@link ContributionItem} for + * @param nodes the set of nodes the action should be applied to + * @return a new {@link ContributionItem} which implements the given action + * on the given nodes */ - private IContributionItem createDynamicMenuToggle( - final MenuAction.Toggle firstAction, - final TreeMap> actionsMap) { - - final boolean isChecked = firstAction.isChecked(); + private ContributionItem createContributionItem(final RuleAction action, + final List nodes) { + if (action instanceof RuleAction.Separator) { + return new Separator(); + } else if (action instanceof NestedAction) { + NestedAction parentAction = (NestedAction) action; + return new ActionContributionItem(new NestedActionMenu(parentAction, nodes)); + } else if (action instanceof Choices) { + Choices parentAction = (Choices) action; + return new ActionContributionItem(new NestedChoiceMenu(parentAction, nodes)); + } else if (action instanceof Toggle) { + return new ActionContributionItem(createToggleAction(action, nodes)); + } else { + return new ActionContributionItem(createPlainAction(action, nodes)); + } + } - Action a = new Action(firstAction.getTitle(), IAction.AS_CHECK_BOX) { + private Action createToggleAction(final RuleAction action, final List nodes) { + Toggle toggleAction = (Toggle) action; + final boolean isChecked = toggleAction.isChecked(); + Action a = new Action(action.getTitle(), IAction.AS_CHECK_BOX) { @Override public void run() { - final List actions = actionsMap.get(firstAction.getId()); - if (actions == null || actions.isEmpty()) { - return; - } - - String label = String.format("Toggle attribute %s", actions.get(0).getTitle()); - if (actions.size() > 1) { - label += String.format(" (%d elements)", actions.size()); - } - - if (mEditor.isEditXmlModelPending()) { - // This should not be happening. - logError("Action '%s' failed: XML changes pending, document might be corrupt.", //$NON-NLS-1$ - label); - return; - } - + String label = createActionLabel(action, nodes); mEditor.wrapUndoEditXmlModel(label, new Runnable() { public void run() { - // Invoke the callbacks of all the actions using the same action-id - for (MenuAction a2 : actions) { - if (a2 instanceof MenuAction.Action) { - IMenuCallback c = ((MenuAction.Action) a2).getCallback(); - if (c != null) { - try { - c.action(a2, null /* no valueId for a toggle */, - !isChecked); - } catch (Exception e) { - AdtPlugin.log(e, "XML edit operation failed: %s", - e.toString()); - } - } - } - } + action.getCallback().action(action, nodes, + null/* no valueId for a toggle */, !isChecked); + applyPendingChanges(); } }); } }; - a.setId(firstAction.getId()); + a.setId(action.getId()); a.setChecked(isChecked); - - return new ActionContributionItem(a); + return a; } - /** - * Invoked by {@link #populateDynamicContextMenu()} to create a new menu item - * for a plain action. This is nearly identical to {@link #createDynamicMenuToggle}, - * except for the {@link IAction} type and the removal of setChecked, isChecked, etc. - * - * @param firstAction The action to convert to a menu item. In the case of a - * multiple selection, this is the first of many similar actions. - * @param actionsMap Map of all contributed actions. - * @return a new {@link IContributionItem} to add to the context menu - */ - private IContributionItem createDynamicAction( - final MenuAction.Action firstAction, - final TreeMap> actionsMap) { - - Action a = new Action(firstAction.getTitle(), IAction.AS_PUSH_BUTTON) { + private IAction createPlainAction(final RuleAction action, final List nodes) { + IAction a = new Action(action.getTitle(), IAction.AS_PUSH_BUTTON) { @Override public void run() { - final List actions = actionsMap.get(firstAction.getId()); - if (actions == null || actions.isEmpty()) { - return; - } - - String label = actions.get(0).getTitle(); - if (actions.size() > 1) { - label += String.format(" (%d elements)", actions.size()); - } - - if (mEditor.isEditXmlModelPending()) { - // This should not be happening. - logError("Action '%s' failed: XML changes pending, document might be corrupt.", //$NON-NLS-1$ - label); - return; - } - + String label = createActionLabel(action, nodes); mEditor.wrapUndoEditXmlModel(label, new Runnable() { public void run() { - // Invoke the callbacks of all the actions using the same action-id - for (MenuAction a2 : actions) { - if (a2 instanceof MenuAction.Action) { - IMenuCallback c = ((MenuAction.Action) a2).getCallback(); - if (c != null) { - try { - // Values do not apply for plain actions - c.action(a2, null /* valueId */, null /* newValue */); - } catch (Exception e) { - AdtPlugin.log(e, "XML edit operation failed: %s", - e.toString()); - } - } - } - } + action.getCallback().action(action, nodes, null, + Boolean.TRUE); + applyPendingChanges(); } }); } }; - a.setId(firstAction.getId()); + a.setId(action.getId()); + return a; + } - return new ActionContributionItem(a); + private static String createActionLabel(final RuleAction action, final List nodes) { + String label = action.getTitle(); + if (nodes.size() > 1) { + label += String.format(" (%d elements)", nodes.size()); + } + return label; } /** - * Invoked by {@link #populateDynamicContextMenu()} to create a new menu item - * for a {@link MenuAction.Choices}. - *

- * Multiple-choices are represented by a sub-menu containing checked items. - * - * @param firstAction The choices action to convert to a menu item. In the case of a - * multiple selection, this is the first of many similar actions. - * @param actionsMap Map of all contributed actions. - * @return a new {@link IContributionItem} to add to the context menu + * The {@link NestedParentMenu} provides submenu content which adds actions + * available on one of the selected node's parent nodes. This will be + * similar to the menu content for the selected node, except the parent + * menus will not be embedded within the nested menu. */ - private IContributionItem createDynamicChoices( - final MenuAction.Choices firstAction, - Map choiceMap, - final TreeMap> actionsMap) { + private class NestedParentMenu extends SubmenuAction { + INode mParent; - IconFactory factory = IconFactory.getInstance(); - MenuManager submenu = new MenuManager(firstAction.getTitle(), firstAction.getId()); - - // Convert to a tree map as needed so that keys be naturally ordered. - if (!(choiceMap instanceof TreeMap)) { - choiceMap = new TreeMap(choiceMap); + NestedParentMenu(String title, INode parent) { + super(title); + mParent = parent; } - String sepPattern = Pattern.quote(MenuAction.Choices.CHOICE_SEP); - - for (Entry entry : choiceMap.entrySet() ) { - final String key = entry.getKey(); - String title = entry.getValue(); - - if (key == null || title == null) { - continue; + @Override + protected void addMenuItems(Menu menu) { + List selection = mCanvas.getSelectionManager().getSelections(); + if (selection.size() == 0) { + return; } - if (MenuAction.Choices.SEPARATOR.equals(title)) { - submenu.add(new Separator()); - continue; + List menuItems = getMenuItems(Collections.singletonList(mParent)); + for (IContributionItem menuItem : menuItems) { + menuItem.fill(menu, -1); } + } + } - final List actions = actionsMap.get(firstAction.getId()); + /** + * The {@link NestedActionMenu} creates a lazily populated pull-right menu + * where the children are {@link RuleAction}'s themselves. + */ + private class NestedActionMenu extends SubmenuAction { + private final NestedAction mParentAction; + private final List mNodes; - if (actions == null || actions.isEmpty()) { - continue; + NestedActionMenu(NestedAction parentAction, List nodes) { + super(parentAction.getTitle()); + mParentAction = parentAction; + mNodes = nodes; + + assert mNodes.size() > 0; + } + + @Override + protected void addMenuItems(Menu menu) { + Map> allActions = new HashMap>(); + for (INode node : mNodes) { + List actionList = mParentAction.getNestedActions(node); + allActions.put(node, actionList); } - // Are all actions for this id checked, unchecked, or in a mixed state? - int numOff = 0; - int numOn = 0; - for (MenuAction a2 : actions) { - MenuAction.Choices choice = (MenuAction.Choices) a2; - String current = choice.getCurrent(); - if (current == null) { - // None of the choices were selected. This can for example happen if - // the user does not have an attribute for "layout_width" set on the element - // and the context menu is opened to see the width choices. - numOff++; + Set availableIds = computeApplicableActionIds(allActions); + List firstSelectedActions = allActions.get(mNodes.get(0)); + + for (RuleAction firstAction : firstSelectedActions) { + if (!availableIds.contains(firstAction.getId()) + && !(firstAction instanceof RuleAction.Separator)) { + // This action isn't supported by all selected items. continue; } - boolean found = false; - - if (current.indexOf(MenuAction.Choices.CHOICE_SEP) >= 0) { - // current choice has a separator, so it's a flag with multiple values - // selected. Compare keys with the split values. - if (current.indexOf(key) >= 0) { - for(String value : current.split(sepPattern)) { - if (key.equals(value)) { - found = true; - break; - } - } - } - } else { - // current choice has no separator, simply compare to the key - found = key.equals(current); - } - if (found) { - numOn++; - } else { - numOff++; - } + createContributionItem(firstAction, mNodes).fill(menu, -1); } + } + } - // We consider the item to be checked if all actions are all checked. - // This means a mixed item will be first toggled from off to on by all the callbacks. - final boolean isChecked = numOff == 0 && numOn > 0; - boolean isMixed = numOff > 0 && numOn > 0; - - if (isMixed) { - title += String.format(" (%1$d/%2$d)", numOn, numOff + numOn); + private void applyPendingChanges() { + LayoutCanvas canvas = mEditor.getGraphicalEditor().getCanvasControl(); + CanvasViewInfo root = canvas.getViewHierarchy().getRoot(); + if (root != null) { + UiViewElementNode uiViewNode = root.getUiViewNode(); + NodeFactory nodeFactory = canvas.getNodeFactory(); + NodeProxy rootNode = nodeFactory.create(uiViewNode); + if (rootNode != null) { + rootNode.applyPendingChanges(); } + } + } - Action a = new Action(title, IAction.AS_CHECK_BOX) { - @Override - public void run() { - - String label = - String.format("Change attribute %1$s", actions.get(0).getTitle()); - if (actions.size() > 1) { - label += String.format(" (%1$d elements)", actions.size()); - } + /** + * The {@link NestedChoiceMenu} creates a lazily populated pull-right menu + * where the items in the menu are strings + */ + private class NestedChoiceMenu extends SubmenuAction { + private final Choices mParentAction; + private final List mNodes; + + NestedChoiceMenu(Choices parentAction, List nodes) { + super(parentAction.getTitle()); + mParentAction = parentAction; + mNodes = nodes; + } - if (mEditor.isEditXmlModelPending()) { - // This should not be happening. - logError("Action '%1$s' failed: XML changes pending, document might be corrupt.", //$NON-NLS-1$ - label); - return; - } + @Override + protected void addMenuItems(Menu menu) { + List titles = mParentAction.getTitles(); + List ids = mParentAction.getIds(); + String current = mParentAction.getCurrent(); + assert titles.size() == ids.size(); + String[] currentValues = current != null + && current.indexOf(RuleAction.CHOICE_SEP) != -1 ? + current.split(RuleAction.CHOICE_SEP_PATTERN) : null; + for (int i = 0, n = Math.min(titles.size(), ids.size()); i < n; i++) { + final String id = ids.get(i); + if (id == null || id.equals(RuleAction.SEPARATOR)) { + new Separator().fill(menu, -1); + continue; + } - mEditor.wrapUndoEditXmlModel(label, new Runnable() { - public void run() { - // Invoke the callbacks of all the actions using the same action-id - for (MenuAction a2 : actions) { - if (a2 instanceof MenuAction.Action) { - try { - ((MenuAction.Action) a2).getCallback().action(a2, key, - !isChecked); - } catch (Exception e) { - AdtPlugin.log(e, "XML edit operation failed: %s", - e.toString()); - } + // Find out whether this item is selected + boolean select = false; + if (current != null) { + // The current choice has a separator, so it's a flag with + // multiple values selected. Compare keys with the split + // values. + if (currentValues != null) { + if (current.indexOf(id) >= 0) { + for (String value : currentValues) { + if (id.equals(value)) { + select = true; + break; } } } - }); + } else { + // current choice has no separator, simply compare to the key + select = id.equals(current); + } } - }; - a.setId(String.format("%1$s_%2$s", firstAction.getId(), key)); //$NON-NLS-1$ - a.setChecked(isChecked); - if (isMixed) { - a.setImageDescriptor(factory.getImageDescriptor("match_multiple")); //$NON-NLS-1$ - } - - submenu.add(a); - } - return submenu; - } + String title = titles.get(i); + IAction a = new Action(title, IAction.AS_PUSH_BUTTON) { + @Override + public void runWithEvent(Event event) { + run(); + } + @Override + public void run() { + String label = createActionLabel(mParentAction, mNodes); + mEditor.wrapUndoEditXmlModel(label, new Runnable() { + public void run() { + mParentAction.getCallback().action(mParentAction, mNodes, id, + Boolean.TRUE); + applyPendingChanges(); + } + }); + } + }; + a.setId(id); + a.setEnabled(true); + if (select) { + a.setChecked(true); + } - private void logError(String format, Object...args) { - AdtPlugin.logAndPrintError( - null, // exception - mCanvas.getRulesEngine().getProject().getName(), // tag - format, args); + new ActionContributionItem(a).fill(menu, -1); + } + } } - } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java index 151a240..74960cf 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/GestureManager.java @@ -184,24 +184,6 @@ public class GestureManager { } /** - * Returns the {@link DropTargetListener} used by the GestureManager. This - * is a bit leaky, but the Outline is reusing all this code... This should - * be separated out. - */ - /* package */DropTargetListener getDropTargetListener() { - return mDropListener; - } - - /** - * Returns the {@link DragSourceListener} used by the GestureManager. This - * is a bit leaky, but the Outline is reusing all this code... This should - * be separated out. - */ - /* package */DragSourceListener getDragSourceListener() { - return mDragSourceListener; - } - - /** * Registers all the listeners needed by the {@link GestureManager}. * * @param dragSource The drag source in the {@link LayoutCanvas} to listen diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java index 0dcd83e..844c8d3 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/LayoutActionBar.java @@ -18,12 +18,12 @@ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; -import com.android.ide.common.api.MenuAction; -import com.android.ide.common.api.MenuAction.OrderedChoices; -import com.android.ide.common.api.MenuAction.Separator; -import com.android.ide.common.api.MenuAction.Toggle; +import com.android.ide.common.api.INode; +import com.android.ide.common.api.RuleAction; +import com.android.ide.common.api.RuleAction.Choices; +import com.android.ide.common.api.RuleAction.Separator; +import com.android.ide.common.api.RuleAction.Toggle; import com.android.ide.common.layout.BaseViewRule; -import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.IconFactory; import com.android.ide.eclipse.adt.internal.editors.layout.configuration.ConfigurationComposite; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; @@ -31,7 +31,6 @@ import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.sdkuilib.internal.widgets.ResolutionChooserDialog; -import org.eclipse.core.runtime.IStatus; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; @@ -67,6 +66,13 @@ public class LayoutActionBar extends Composite { private ToolItem mZoomInButton; private ToolItem mZoomFitButton; + /** + * Creates a new {@link LayoutActionBar} and adds it to the given parent. + * + * @param parent the parent composite to add the actions bar to + * @param style the SWT style to apply + * @param editor the associated layout editor + */ public LayoutActionBar(Composite parent, int style, GraphicalEditorPart editor) { super(parent, style | SWT.NO_FOCUS); mEditor = editor; @@ -118,7 +124,7 @@ public class LayoutActionBar extends Composite { for (SelectionItem item : selections) { selectedNodes.add(item.getNode()); } - List actions = new ArrayList(); + List actions = new ArrayList(); engine.callAddLayoutActions(actions, parent, selectedNodes); // Place actions in the correct order (the actions may come from different @@ -131,14 +137,14 @@ public class LayoutActionBar extends Composite { int index = -1; String label = null; if (selectedNodes.size() == 1) { - List itemActions = new ArrayList(); + List itemActions = new ArrayList(); NodeProxy selectedNode = selectedNodes.get(0); engine.callAddLayoutActions(itemActions, selectedNode, null); if (itemActions.size() > 0) { Collections.sort(itemActions); - if (!(itemActions.get(0) instanceof MenuAction.Separator)) { - actions.add(MenuAction.createSeparator(0)); + if (!(itemActions.get(0) instanceof RuleAction.Separator)) { + actions.add(RuleAction.createSeparator(0)); } label = selectedNode.getStringAttr(ANDROID_URI, ATTR_ID); if (label != null) { @@ -155,7 +161,7 @@ public class LayoutActionBar extends Composite { mLayoutToolBar.layout(); } - private void addActions(List actions, int labelIndex, String label) { + private void addActions(List actions, int labelIndex, String label) { if (actions.size() > 0) { // Flag used to indicate that if there are any actions -after- this, it // should be separated from this current action (we don't unconditionally @@ -164,7 +170,7 @@ public class LayoutActionBar extends Composite { boolean needSeparator = false; int index = 0; - for (MenuAction action : actions) { + for (RuleAction action : actions) { if (index == labelIndex) { final ToolItem button = new ToolItem(mLayoutToolBar, SWT.PUSH); button.setText(label); @@ -181,8 +187,8 @@ public class LayoutActionBar extends Composite { needSeparator = false; } - if (action instanceof MenuAction.OrderedChoices) { - MenuAction.OrderedChoices choices = (OrderedChoices) action; + if (action instanceof RuleAction.Choices) { + RuleAction.Choices choices = (Choices) action; if (!choices.isRadio()) { addDropdown(choices); } else { @@ -190,13 +196,10 @@ public class LayoutActionBar extends Composite { addRadio(choices); needSeparator = true; } - } else if (action instanceof MenuAction.Toggle) { + } else if (action instanceof RuleAction.Toggle) { addToggle((Toggle) action); - } else if (action instanceof MenuAction.Action) { - addAction((MenuAction.Action) action); } else { - AdtPlugin.log(IStatus.ERROR, "Action not supported in toolbar: %1$s", - action.getTitle()); + addPlainAction(action); } } } @@ -226,8 +229,8 @@ public class LayoutActionBar extends Composite { button.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { - toggle.getCallback().action(toggle, toggle.getId(), - button.getSelection()); + toggle.getCallback().action(toggle, getSelectedNodes(), + toggle.getId(), button.getSelection()); updateSelection(); } }); @@ -236,7 +239,19 @@ public class LayoutActionBar extends Composite { } } - private void addAction(final MenuAction.Action menuAction) { + private List getSelectedNodes() { + List selections = + mEditor.getCanvasControl().getSelectionManager().getSelections(); + List nodes = new ArrayList(selections.size()); + for (SelectionItem item : selections) { + nodes.add(item.getNode()); + } + + return nodes; + } + + + private void addPlainAction(final RuleAction menuAction) { final ToolItem button = new ToolItem(mLayoutToolBar, SWT.PUSH); URL iconUrl = menuAction.getIconUrl(); @@ -251,13 +266,14 @@ public class LayoutActionBar extends Composite { button.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { - menuAction.getCallback().action(menuAction, menuAction.getId(), false); + menuAction.getCallback().action(menuAction, getSelectedNodes(), menuAction.getId(), + false); updateSelection(); } }); } - private void addRadio(final MenuAction.OrderedChoices choices) { + private void addRadio(final RuleAction.Choices choices) { List icons = choices.getIconUrls(); List titles = choices.getTitles(); List ids = choices.getIds(); @@ -277,7 +293,7 @@ public class LayoutActionBar extends Composite { @Override public void widgetSelected(SelectionEvent e) { if (item.getSelection()) { - choices.getCallback().action(choices, id, null); + choices.getCallback().action(choices, getSelectedNodes(), id, null); updateSelection(); } } @@ -289,7 +305,7 @@ public class LayoutActionBar extends Composite { } } - private void addDropdown(final MenuAction.OrderedChoices choices) { + private void addDropdown(final RuleAction.Choices choices) { final ToolItem combo = new ToolItem(mLayoutToolBar, SWT.DROP_DOWN); URL iconUrl = choices.getIconUrl(); if (iconUrl != null) { @@ -331,7 +347,7 @@ public class LayoutActionBar extends Composite { item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { - choices.getCallback().action(choices, id, null); + choices.getCallback().action(choices, getSelectedNodes(), id, null); updateSelection(); } }); 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 32cc45d..1e6d958 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 @@ -1047,11 +1047,8 @@ public class LayoutCanvas extends Canvas { /** * Helper to create the drop target for the given control. - *

- * This is static with package-access so that {@link OutlinePage} can also - * create an exact copy of the drop target with the same attributes. */ - /* package */static DropTarget createDropTarget(Control control) { + private static DropTarget createDropTarget(Control control) { DropTarget dropTarget = new DropTarget( control, DND.DROP_COPY | DND.DROP_MOVE | DND.DROP_DEFAULT); dropTarget.setTransfer(new Transfer[] { diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java index 4ee3aac..770f0eb 100755 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PaletteControl.java @@ -23,7 +23,7 @@ import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT; import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; import com.android.ide.common.api.InsertType; -import com.android.ide.common.api.MenuAction.Toggle; +import com.android.ide.common.api.RuleAction.Toggle; import com.android.ide.common.api.Rect; import com.android.ide.common.rendering.LayoutLibrary; import com.android.ide.common.rendering.api.Capability; 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 15f4379..9cf683a 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 @@ -25,7 +25,7 @@ import com.android.ide.common.api.IGraphics; import com.android.ide.common.api.INode; import com.android.ide.common.api.IViewRule; import com.android.ide.common.api.InsertType; -import com.android.ide.common.api.MenuAction; +import com.android.ide.common.api.RuleAction; import com.android.ide.common.api.Point; import com.android.ide.common.api.Rect; import com.android.ide.common.api.SegmentType; @@ -52,6 +52,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -211,21 +212,24 @@ public class RulesEngine { } /** - * Invokes {@link IViewRule#getContextMenu(INode)} on the rule matching the specified element. + * Invokes {@link IViewRule#addContextMenuActions(List, 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}. + * any custom menu actions. Otherwise, a list of {@link RuleAction}. */ - public List callGetContextMenu(NodeProxy selectedNode) { + public List callGetContextMenu(NodeProxy selectedNode) { // try to find a rule for this element's FQCN IViewRule rule = loadRule(selectedNode.getNode()); if (rule != null) { try { mInsertType = InsertType.CREATE; - return rule.getContextMenu(selectedNode); + List actions = new ArrayList(); + rule.addContextMenuActions(actions, selectedNode); + Collections.sort(actions); + return actions; } catch (Exception e) { AdtPlugin.log(e, "%s.getContextMenu() failed: %s", rule.getClass().getSimpleName(), @@ -237,16 +241,18 @@ public class RulesEngine { } /** - * Invokes {@link IViewRule#getContextMenu(INode)} on the rule matching the specified element. + * Invokes {@link IViewRule#addLayoutActions(List, INode, List)} on the rule + * matching the specified element. * * @param actions The list of actions to add layout actions into * @param parentNode The layout node - * @param children The selected children of the node, if any (used to initialize values - * of child layout controls, if applicable) - * @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}. + * @param children The selected children of the node, if any (used to + * initialize values of child layout controls, if applicable) + * @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 RuleAction}. */ - public List callAddLayoutActions(List actions, + public List callAddLayoutActions(List actions, NodeProxy parentNode, List children ) { // try to find a rule for this element's FQCN IViewRule rule = loadRule(parentNode.getNode()); -- cgit v1.1