/*
* 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.
* <p/>
* Each action should have a reasonably unique ID. By default actions are stored using
* the lexicographical order of the IDs.
* Duplicated IDs will be ignored -- that is the first one found will be used.
* <p/>
* When the canvas has a multiple selection, only actions that are present in <em>all</em>
* the selected nodes are shown. Moreover, for a given ID all actions must be equal, for
* example they must have the same title and choice but not necessarily the same selection. <br/>
* This allows the canvas to only display compatible actions that will work on all selected
* elements.
* <p/>
* Actions can be grouped in sub-menus if necessary. Whether groups (sub-menus) can contain
* other groups is implementation dependent. Currently the canvas does not support this, but
* we may decide to change this behavior later if deemed useful.
* <p/>
* All actions and groups are sorted by their ID, using String's natural sorting order.
* The only way to change this sorting is by choosing the IDs so they the result end up
* sorted as you want it.
* <p/>
* The {@link MenuAction} is abstract. Users should instantiate either {@link Toggle},
* {@link Choices} or {@link Group} instead. These classes are immutable.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
* </p>
*/
public abstract class MenuAction implements Comparable<MenuAction> {
/**
* 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<String> titles, List<URL> iconUrls, List<String> 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<String> titles, List<URL> iconUrls, List<String> 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<MenuAciton>
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.
* <p/>
* Note that group can be seen as a "group declaration": the group does not hold a list
* actions that it will contain. This merely let the canvas create a sub-menu with the
* given title and actions that define this group-id will be placed in the sub-menu.
* <p/>
* The current canvas has the following implementation details: <br/>
* - There's only one level of sub-menu.
* That is you can't have a sub-menu inside another sub-menu.
* This is expressed by the fact that groups do not have a parent group-id. <br/>
* - It is not currently necessary to define a group before defining actions that refer
* to that group. Moreover, in the context of a multiple selection, one view could
* contribute actions to any group even if created by another view. Both practices
* are discouraged. <br/>
* - Actions which group-id do not match any known group will simply be placed in the
* root context menu. <br/>
* - Empty groups do not create visible sub-menus. <br/>
* These implementations details may change in the future and should not be relied upon.
*/
public static class Group extends MenuAction {
/**
* Constructs a new group of actions.
*
* @param id The id of the group. Must be unique. Cannot be null.
* @param title The UI-visible title of the group, shown in the sub-menu.
*/
public Group(String id, String title) {
super(id, title);
}
}
/**
* The base class for {@link Toggle} and {@link Choices}.
*/
public static 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.
* <p/>
* 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<String> mTitles;
protected List<URL> mIconUrls;
protected List<String> 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<String> titles, List<URL> iconUrls, List<String> ids, String current) {
super(id, title, groupId, callback);
mTitles = titles;
mIconUrls = iconUrls;
mIds = ids;
mCurrent = current;
}
public List<URL> getIconUrls() {
return mIconUrls;
}
public List<String> getIds() {
return mIds;
}
public List<String> 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<String> titles, List<URL> iconUrls, List<String> 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<String>();
mIconUrls = new ArrayList<URL>();
mIds = new ArrayList<String>();
mProvider.addChoices(mTitles, mIconUrls, mIds);
}
}
@Override
public List<URL> getIconUrls() {
ensureInitialized();
return mIconUrls;
}
@Override
public List<String> getIds() {
ensureInitialized();
return mIds;
}
@Override
public List<String> 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.
* <p/>
* Implementation detail: empty choices will not be displayed in the context menu.
* <p/>
* Choice items are sorted by their id, using String's natural sorting order.
* <p/>
* 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<String, String> mChoices;
/**
* One or more id for the checked choice(s) that will be check marked.
* Can be null. Can be an id not present in the choices map.
* If more than one choice, they must be separated by {@link #CHOICE_SEP}.
*/
private final String mCurrent;
/**
* Creates a new immutable multiple-choice action.
* This action has no group-id and will show up in the root of the context menu.
*
* @param id The unique id of the action. Cannot be null.
* @param title The UI-visible title of the context menu item. Cannot be null.
* @param choices A map id=>title for all the multiple-choice items. Cannot be null.
* @param current The id(s) of the current choice(s) that will be check marked.
* Can be null. Can be an id not present in the choices map.
* There can be more than one id separated by {@link #CHOICE_SEP}.
* @param callback A callback to execute when the context menu item is
* selected.
*/
public Choices(String id, String title,
Map<String, String> 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<String, String> 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<String, String> getChoices() {
return mChoices;
}
/**
* Returns the id(s) of the current choice(s) that are check marked.
* Can be null. Can be an id not present in the choices map.
* There can be more than one id separated by {@link #CHOICE_SEP}.
*/
public String getCurrent() {
return mCurrent;
}
/**
* Two multiple choices are equal if they have the same id, title, group-id and choices.
* It is acceptable for the current state and action 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;
}
}
}