/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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.internal.view.menu; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Vector; import com.android.internal.view.menu.MenuView; import android.app.Activity; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.ContextMenu.ContextMenuInfo; import android.widget.BaseAdapter; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; /** * Interface for managing the items in a menu. * <p> * By default, every Activity supports an options menu of actions or options. * You can add items to this menu and handle clicks on your additions. The * easiest way of adding menu items is inflating an XML file into the * {@link MenuBuilder} via {@link MenuInflater}. The easiest way of attaching * code to clicks is via {@link Activity#onOptionsItemSelected(MenuItemImpl)} * and {@link Activity#onContextItemSelected(MenuItemImpl)}. * <p> * Different menu types support different features: * <ol> * <li><b>Context menus</b>: Do not support item shortcuts and item icons. * <li><b>Options menus</b>: The <b>icon menus</b> do not support item check * marks and only show the item's * {@link MenuItemImpl#setTitleCondensed(CharSequence) condensed title}. The * <b>expanded menus</b> (only available if six or more menu items are visible, * reached via the 'More' item in the icon menu) do not show item icons, and * item check marks are discouraged. * <li><b>Sub menus</b>: Do not support item icons, or nested sub menus. * </ol> */ public class MenuBuilder implements Menu { private static final String TAG = "MenuBuilder"; /** The number of different menu types */ public static final int NUM_TYPES = 3; /** The menu type that represents the icon menu view */ public static final int TYPE_ICON = 0; /** The menu type that represents the expanded menu view */ public static final int TYPE_EXPANDED = 1; /** * The menu type that represents a menu dialog. Examples are context and sub * menus. This menu type will not have a corresponding MenuView, but it will * have an ItemView. */ public static final int TYPE_DIALOG = 2; private static final String VIEWS_TAG = "android:views"; // Order must be the same order as the TYPE_* static final int THEME_RES_FOR_TYPE[] = new int[] { com.android.internal.R.style.Theme_IconMenu, com.android.internal.R.style.Theme_ExpandedMenu, 0, }; // Order must be the same order as the TYPE_* static final int LAYOUT_RES_FOR_TYPE[] = new int[] { com.android.internal.R.layout.icon_menu_layout, com.android.internal.R.layout.expanded_menu_layout, 0, }; // Order must be the same order as the TYPE_* static final int ITEM_LAYOUT_RES_FOR_TYPE[] = new int[] { com.android.internal.R.layout.icon_menu_item_layout, com.android.internal.R.layout.list_menu_item_layout, com.android.internal.R.layout.list_menu_item_layout, }; private static final int[] sCategoryToOrder = new int[] { 1, /* No category */ 4, /* CONTAINER */ 5, /* SYSTEM */ 3, /* SECONDARY */ 2, /* ALTERNATIVE */ 0, /* SELECTED_ALTERNATIVE */ }; private final Context mContext; private final Resources mResources; /** * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode() * instead of accessing this directly. */ private boolean mQwertyMode; /** * Callback that will receive the various menu-related events generated by * this class. Use getCallback to get a reference to the callback. */ private Callback mCallback; /** Contains all of the items for this menu */ private ArrayList<MenuItemImpl> mItems; /** * Contains only the items that are currently visible. This will be * created/refreshed from {@link #getVisibleItems()} */ private ArrayList<MenuItemImpl> mVisibleItems; /** * Whether or not the items (or any one item's shown state) has changed * since it was last fetched from {@link #getVisibleItems()} */ private boolean mIsVisibleItemsStale; /** Header title for menu types that have a header (context and submenus) */ CharSequence mHeaderTitle; /** * Header icon for menu types that have a header and support icons (context) */ Drawable mHeaderIcon; /** * Header custom view for menu types that have a header and support custom * views (context) */ View mHeaderView; /** * Contains the state of the View hierarchy for all menu views when the menu * was frozen. */ private SparseArray<Parcelable> mFrozenViewStates; /** * Prevents onItemsChanged from doing its junk, useful for batching commands * that may individually call onItemsChanged. */ private boolean mPreventDispatchingItemsChanged = false; private boolean mOptionalIconsVisible = false; private MenuType[] mMenuTypes; private boolean mShortcutsVisible; /** * Current use case is Context Menus: As Views populate the context menu, * each one has extra information that should be passed along. This is the * current menu info that should be set on all items added to this menu. */ private ContextMenuInfo mCurrentMenuInfo; private static final Class[] mConstructorSignature = new Class[] { Context.class, AttributeSet.class }; class MenuType { private int mMenuType; /** The layout inflater that uses the menu type's theme */ private LayoutInflater mInflater; /** The lazily loaded {@link MenuView} */ private WeakReference<MenuView> mMenuView; MenuType(int menuType) { mMenuType = menuType; } LayoutInflater getInflater() { // Create an inflater that uses the given theme for the Views it // inflates if (mInflater == null) { mInflater = (LayoutInflater) Context.getSystemContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); } return mInflater; } MenuView getMenuView(ViewGroup parent) { if (LAYOUT_RES_FOR_TYPE[mMenuType] == 0) { return null; } MenuView menuView = mMenuView != null ? mMenuView.get() : null; if (menuView == null) { // menuView = (MenuView) getInflater().inflate( // LAYOUT_RES_FOR_TYPE[mMenuType], parent, false); menuView = (MenuView) ((LayoutInflater) Context .getSystemContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE)).inflate( LAYOUT_RES_FOR_TYPE[mMenuType], parent, false); menuView.initialize(MenuBuilder.this, mMenuType); // Cache the view mMenuView = new WeakReference<MenuView>(menuView); if (mFrozenViewStates != null) { View view = (View) menuView; view.restoreHierarchyState(mFrozenViewStates); // Clear this menu type's frozen state, since we just // restored it mFrozenViewStates.remove(view.getId()); } } return menuView; } boolean hasMenuView() { return mMenuView != null && mMenuView.get() != null; } } /** * Called by menu to notify of close and selection changes */ public interface Callback { /** * Called when a menu item is selected. * * @param menu * The menu that is the parent of the item * @param item * The menu item that is selected * @return whether the menu item selection was handled */ public boolean onMenuItemSelected(MenuBuilder menu, MenuItemImpl item); /** * Called when a menu is closed. * * @param menu * The menu that was closed. * @param allMenusAreClosing * Whether the menus are completely closing (true), or * whether there is another menu opening shortly (false). For * example, if the menu is closing because a sub menu is * about to be shown, <var>allMenusAreClosing</var> is false. */ public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing); /** * Called when a sub menu is selected. This is a cue to open the given * sub menu's decor. * * @param subMenu * the sub menu that is being opened * @return whether the sub menu selection was handled by the callback */ public boolean onSubMenuSelected(SubMenuBuilder subMenu); /** * Called when a sub menu is closed * * @param menu * the sub menu that was closed */ public void onCloseSubMenu(SubMenuBuilder menu); /** * Called when the mode of the menu changes (for example, from icon to * expanded). * * @param menu * the menu that has changed modes */ public void onMenuModeChange(MenuBuilder menu); } /** * Called by menu items to execute their associated action */ public interface ItemInvoker { public boolean invokeItem(MenuItemImpl item); } public MenuBuilder(Context context) { mMenuTypes = new MenuType[NUM_TYPES]; mContext = context; mResources = context.getResources(); mItems = new ArrayList<MenuItemImpl>(); mVisibleItems = new ArrayList<MenuItemImpl>(); mIsVisibleItemsStale = true; } public void setCallback(Callback callback) { mCallback = callback; } public MenuType getMenuType(int menuType) { if (mMenuTypes[menuType] == null) { mMenuTypes[menuType] = new MenuType(menuType); } return mMenuTypes[menuType]; } /** * Gets a menu View that contains this menu's items. * * @param menuType * The type of menu to get a View for (must be one of * {@link #TYPE_ICON}, {@link #TYPE_EXPANDED}, * {@link #TYPE_DIALOG}). * @param parent * The ViewGroup that provides a set of LayoutParams values for * this menu view * @return A View for the menu of type <var>menuType</var> */ public View getMenuView(int menuType, ViewGroup parent) { if (menuType == TYPE_EXPANDED && (mMenuTypes[TYPE_ICON] == null || !mMenuTypes[TYPE_ICON].hasMenuView())) { getMenuType(TYPE_ICON).getMenuView(parent); } return (View) getMenuType(menuType).getMenuView(parent); } /** * Adds an item to the menu. The other add methods funnel to this. */ private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) { final int ordering = getOrdering(categoryOrder); final MenuItemImpl item = new MenuItemImpl(this, group, id, categoryOrder, ordering, title); if (mItems == null) { mItems = new ArrayList<MenuItemImpl>(); } mItems.add(findInsertIndex(mItems, ordering), item); onItemsChanged(false); return item; } public MenuItem add(CharSequence title) { return addInternal(0, 0, 0, title); } public MenuItem add(int titleRes) { return addInternal(0, 0, 0, mResources.getString(titleRes)); } public MenuItem add(int group, int id, int categoryOrder, CharSequence title) { return addInternal(group, id, categoryOrder, title); } public MenuItem add(int group, int id, int categoryOrder, int title) { return addInternal(group, id, categoryOrder, mResources.getString(title)); } public SubMenu addSubMenu(CharSequence title) { return addSubMenu(0, 0, 0, title); } public SubMenu addSubMenu(int titleRes) { return addSubMenu(0, 0, 0, mResources.getString(titleRes)); } public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) { try { final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title); final SubMenuBuilder subMenu = (SubMenuBuilder) Class.forName( "com.android.internal.view.menu.SubMenuBuilder").newInstance(); subMenu.setmItem(item); subMenu.setmParentMenu(this); item.setSubMenu(subMenu); return subMenu; } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } catch (InstantiationException e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } catch (SecurityException e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } } public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) { return addSubMenu(group, id, categoryOrder, mResources.getString(title)); } public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) { PackageManager pm = mContext.getPackageManager(); final ContentResolver resolver = mContext.getContentResolver(); String[] specificTypes = null; if (specifics != null) { final int N = specifics.length; for (int i = 0; i < N; i++) { Intent sp = specifics[i]; if (sp != null) { String t = sp.resolveTypeIfNeeded(resolver); if (t != null) { if (specificTypes == null) { specificTypes = new String[N]; } specificTypes[i] = t; } } } } final List<ResolveInfo> lri = pm.queryIntentActivityOptions(caller, specifics, specificTypes, intent, intent.resolveTypeIfNeeded(resolver), flags); final int N = lri != null ? lri.size() : 0; if ((flags & FLAG_APPEND_TO_GROUP) == 0) { removeGroup(group); } for (int i = 0; i < N; i++) { final ResolveInfo ri = lri.get(i); Intent rintent = new Intent(ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]); rintent.setComponent(new ComponentName( ri.activityInfo.applicationInfo.packageName, ri.activityInfo.name)); final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm)).setIcon(ri.loadIcon(pm)).setIntent( rintent); if (outSpecificItems != null && ri.specificIndex >= 0) { outSpecificItems[ri.specificIndex] = item; } } return N; } public void removeItem(int id) { removeItemAtInt(findItemIndex(id), true); } public void removeGroup(int group) { final int i = findGroupIndex(group); if (i >= 0) { final int maxRemovable = mItems.size() - i; int numRemoved = 0; while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) { // Don't force update for each one, this method will do it at // the end removeItemAtInt(i, false); } // Notify menu views onItemsChanged(false); } } /** * Remove the item at the given index and optionally forces menu views to * update. * * @param index * The index of the item to be removed. If this index is invalid * an exception is thrown. * @param updateChildrenOnMenuViews * Whether to force update on menu views. Please make sure you * eventually call this after your batch of removals. */ private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) { if ((index < 0) || (index >= mItems.size())) return; mItems.remove(index); if (updateChildrenOnMenuViews) onItemsChanged(false); } public void removeItemAt(int index) { removeItemAtInt(index, true); } public void clearAll() { mPreventDispatchingItemsChanged = true; clear(); clearHeader(); mPreventDispatchingItemsChanged = false; onItemsChanged(true); } public void clear() { mItems.clear(); onItemsChanged(true); } void setExclusiveItemChecked(MenuItemImpl item) { final int group = item.getGroupId(); final int N = mItems.size(); for (int i = 0; i < N; i++) { MenuItemImpl curItem = mItems.get(i); if (curItem.getGroupId() == group) { if (!curItem.isExclusiveCheckable()) continue; if (!curItem.isCheckable()) continue; // Check the item meant to be checked, uncheck the others (that // are in the group) curItem.setCheckedInt(curItem == item); } } } public void setGroupCheckable(int group, boolean checkable, boolean exclusive) { final int N = mItems.size(); for (int i = 0; i < N; i++) { MenuItemImpl item = mItems.get(i); if (item.getGroupId() == group) { item.setExclusiveCheckable(exclusive); item.setCheckable(checkable); } } } public void setGroupVisible(int group, boolean visible) { final int N = mItems.size(); // We handle the notification of items being changed ourselves, so we // use setVisibleInt rather // than setVisible and at the end notify of items being changed boolean changedAtLeastOneItem = false; for (int i = 0; i < N; i++) { MenuItemImpl item = mItems.get(i); if (item.getGroupId() == group) { if (item.setVisibleInt(visible)) changedAtLeastOneItem = true; } } if (changedAtLeastOneItem) onItemsChanged(false); } public void setGroupEnabled(int group, boolean enabled) { final int N = mItems.size(); for (int i = 0; i < N; i++) { MenuItemImpl item = mItems.get(i); if (item.getGroupId() == group) { item.setEnabled(enabled); } } } public boolean hasVisibleItems() { final int size = size(); for (int i = 0; i < size; i++) { MenuItemImpl item = mItems.get(i); if (item.isVisible()) { return true; } } return false; } public MenuItem findItem(int id) { final int size = size(); for (int i = 0; i < size; i++) { MenuItemImpl item = mItems.get(i); if (item.getItemId() == id) { return item; } else if (item.hasSubMenu()) { MenuItem possibleItem = item.getSubMenu().findItem(id); if (possibleItem != null) { return possibleItem; } } } return null; } public int findItemIndex(int id) { final int size = size(); for (int i = 0; i < size; i++) { MenuItemImpl item = mItems.get(i); if (item.getItemId() == id) { return i; } } return -1; } public int findGroupIndex(int group) { return findGroupIndex(group, 0); } public int findGroupIndex(int group, int start) { final int size = size(); if (start < 0) { start = 0; } for (int i = start; i < size; i++) { final MenuItemImpl item = mItems.get(i); if (item.getGroupId() == group) { return i; } } return -1; } public int size() { return mItems.size(); } public MenuItem getItem(int index) { return mItems.get(index); } public void setQwertyMode(boolean isQwerty) { mQwertyMode = isQwerty; } /** * Returns the ordering across all items. This will grab the category from * the upper bits, find out how to order the category with respect to other * categories, and combine it with the lower bits. * * @param categoryOrder * The category order for a particular item (if it has not been * or/add with a category, the default category is assumed). * @return An ordering integer that can be used to order this item across * all the items (even from other categories). */ private static int getOrdering(int categoryOrder) { final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT; if (index < 0 || index >= sCategoryToOrder.length) { throw new IllegalArgumentException( "order does not contain a valid category."); } return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK); } /** * @return whether the menu shortcuts are in qwerty mode or not */ boolean isQwertyMode() { return mQwertyMode; } Resources getResources() { return mResources; } public Callback getCallback() { return mCallback; } public Context getContext() { return mContext; } private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) { for (int i = items.size() - 1; i >= 0; i--) { MenuItemImpl item = items.get(i); if (item.getOrdering() <= ordering) { return i + 1; } } return 0; } public boolean performIdentifierAction(int id, int flags) { // Look for an item whose identifier is the id. return performItemAction(findItem(id), flags); } public boolean performItemAction(MenuItem item, int flags) { MenuItemImpl itemImpl = (MenuItemImpl) item; if (itemImpl == null || !itemImpl.isEnabled()) { return false; } boolean invoked = itemImpl.invoke(); if (item.hasSubMenu()) { close(false); if (mCallback != null) { // Return true if the sub menu was invoked or the item was // invoked previously invoked = mCallback.onSubMenuSelected((SubMenuBuilder) item .getSubMenu()) || invoked; } } else { if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) { close(true); } } return invoked; } /** * Closes the visible menu. * * @param allMenusAreClosing * Whether the menus are completely closing (true), or whether * there is another menu coming in this menu's place (false). For * example, if the menu is closing because a sub menu is about to * be shown, <var>allMenusAreClosing</var> is false. */ public final void close(boolean allMenusAreClosing) { Callback callback = getCallback(); if (callback != null) { callback.onCloseMenu(this, allMenusAreClosing); } } /** {@inheritDoc} */ public void close() { close(true); } /** * Called when an item is added or removed. * * @param cleared Whether the items were cleared or just changed. */ private void onItemsChanged(boolean cleared) { if (!mPreventDispatchingItemsChanged) { if (mIsVisibleItemsStale == false) mIsVisibleItemsStale = true; if (mMenuTypes == null) { mMenuTypes = new MenuType[NUM_TYPES]; } MenuType[] menuTypes = mMenuTypes; for (int i = 0; i < NUM_TYPES; i++) { if ((menuTypes[i] != null) && (menuTypes[i].hasMenuView())) { MenuView menuView = menuTypes[i].mMenuView.get(); menuView.updateChildren(cleared); } } } } /** * Called by {@link MenuItemImpl} when its visible flag is changed. * * @param item * The item that has gone through a visibility change. */ void onItemVisibleChanged(MenuItemImpl item) { // Notify of items being changed onItemsChanged(false); } protected ArrayList<MenuItemImpl> getVisibleItems() { if (!mIsVisibleItemsStale) return mVisibleItems; if (mVisibleItems == null) { // Refresh the visible items mVisibleItems = new ArrayList<MenuItemImpl>(); } else { mVisibleItems.clear(); } final int itemsSize = mItems.size(); MenuItemImpl item; for (int i = 0; i < itemsSize; i++) { item = mItems.get(i); if (item.isVisible()) mVisibleItems.add(item); } mIsVisibleItemsStale = false; return mVisibleItems; } public ArrayList<MenuItemImpl> getItems() { return mItems; } public void clearHeader() { mHeaderIcon = null; mHeaderTitle = null; mHeaderView = null; onItemsChanged(false); } private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes, final Drawable icon, final View view) { final Resources r = getResources(); if (view != null) { mHeaderView = view; // If using a custom view, then the title and icon aren't used mHeaderTitle = null; mHeaderIcon = null; } else { if (titleRes > 0) { mHeaderTitle = r.getText(titleRes); } else if (title != null) { mHeaderTitle = title; } if (iconRes > 0) { mHeaderIcon = r.getDrawable(iconRes); } else if (icon != null) { mHeaderIcon = icon; } // If using the title or icon, then a custom view isn't used mHeaderView = null; } // Notify of change onItemsChanged(false); } /** * Sets the header's title. This replaces the header view. Called by the * builder-style methods of subclasses. * * @param title * The new title. * @return This Menu so additional setters can be called. */ protected MenuBuilder setHeaderTitleInt(CharSequence title) { setHeaderInternal(0, title, 0, null, null); return this; } /** * Sets the header's title. This replaces the header view. Called by the * builder-style methods of subclasses. * * @param titleRes * The new title (as a resource ID). * @return This Menu so additional setters can be called. */ protected MenuBuilder setHeaderTitleInt(int titleRes) { setHeaderInternal(titleRes, null, 0, null, null); return this; } /** * Sets the header's icon. This replaces the header view. Called by the * builder-style methods of subclasses. * * @param icon * The new icon. * @return This Menu so additional setters can be called. */ protected MenuBuilder setHeaderIconInt(Drawable icon) { setHeaderInternal(0, null, 0, icon, null); return this; } /** * Sets the header's icon. This replaces the header view. Called by the * builder-style methods of subclasses. * * @param iconRes * The new icon (as a resource ID). * @return This Menu so additional setters can be called. */ protected MenuBuilder setHeaderIconInt(int iconRes) { setHeaderInternal(0, null, iconRes, null, null); return this; } /** * Sets the header's view. This replaces the title and icon. Called by the * builder-style methods of subclasses. * * @param view * The new view. * @return This Menu so additional setters can be called. */ protected MenuBuilder setHeaderViewInt(View view) { setHeaderInternal(0, null, 0, null, view); return this; } public CharSequence getHeaderTitle() { return mHeaderTitle; } public Drawable getHeaderIcon() { return mHeaderIcon; } public View getHeaderView() { return mHeaderView; } /** * Gets the root menu (if this is a submenu, find its root menu). * * @return The root menu. */ public MenuBuilder getRootMenu() { return this; } /** * Gets an adapter for providing items and their views. * * @param menuType * The type of menu to get an adapter for. * @return A {@link MenuAdapter} for this menu with the given menu type. */ public MenuAdapter getMenuAdapter(int menuType) { return new MenuAdapter(menuType); } void setOptionalIconsVisible(boolean visible) { mOptionalIconsVisible = visible; } boolean getOptionalIconsVisible() { return mOptionalIconsVisible; } public void saveHierarchyState(Bundle outState) { SparseArray<Parcelable> viewStates = new SparseArray<Parcelable>(); MenuType[] menuTypes = mMenuTypes; for (int i = NUM_TYPES - 1; i >= 0; i--) { if (menuTypes[i] == null) { continue; } if (menuTypes[i].hasMenuView()) { ((View) menuTypes[i].getMenuView(null)) .saveHierarchyState(viewStates); } } outState.putSparseParcelableArray(VIEWS_TAG, viewStates); } public void restoreHierarchyState(Bundle inState) { // Save this for menu views opened later SparseArray<Parcelable> viewStates = mFrozenViewStates = inState .getSparseParcelableArray(VIEWS_TAG); // Thaw those menu views already open MenuType[] menuTypes = mMenuTypes; for (int i = NUM_TYPES - 1; i >= 0; i--) { if (menuTypes[i] == null) { continue; } if (menuTypes[i].hasMenuView()) { ((View) menuTypes[i].getMenuView(null)) .restoreHierarchyState(viewStates); } } } /** * An adapter that allows an {@link AdapterView} to use this * {@link MenuBuilder} as a data source. This adapter will use only the * visible/shown items from the menu. */ public class MenuAdapter extends BaseAdapter { private int mMenuType; public MenuAdapter(int menuType) { mMenuType = menuType; } public int getOffset() { if (mMenuType == TYPE_EXPANDED) { return getNumIconMenuItemsShown(); } else { return 0; } } public int getCount() { return getVisibleItems().size() - getOffset(); } public MenuItemImpl getItem(int position) { return getVisibleItems().get(position + getOffset()); } public long getItemId(int position) { // Since a menu item's ID is optional, we'll use the position as an // ID for the item in the AdapterView return position; } public View getView(int position, View convertView, ViewGroup parent) { return ((MenuItemImpl) getItem(position)).getItemView(mMenuType, parent); } } private int getNumIconMenuItemsShown() { ViewGroup parent = null; if (!mMenuTypes[TYPE_ICON].hasMenuView()) { /* * There isn't an icon menu view instantiated, so when we get it * below, it will lazily instantiate it. We should pass a proper * parent so it uses the layout_ attributes present in the XML * layout file. */ if (mMenuTypes[TYPE_EXPANDED].hasMenuView()) { View expandedMenuView = (View) mMenuTypes[TYPE_EXPANDED].getMenuView(null); parent = (ViewGroup) expandedMenuView.getParent(); } } return ((IconMenuView) getMenuView(TYPE_ICON, parent)).getNumActualItemsShown(); } /** * @return Whether shortcuts should be visible on menus. */ public boolean isShortcutsVisible() { return mShortcutsVisible; } /** * Clears the cached menu views. Call this if the menu views need to another * layout (for example, if the screen size has changed). */ public void clearMenuViews() { for (int i = NUM_TYPES - 1; i >= 0; i--) { if (mMenuTypes[i] != null) { mMenuTypes[i].mMenuView = null; } } for (int i = mItems.size() - 1; i >= 0; i--) { MenuItemImpl item = mItems.get(i); if (item.hasSubMenu()) { ((SubMenuBuilder) item.getSubMenu()).clearMenuViews(); } item.clearItemViews(); } } @Override public boolean performShortcut(int keyCode, KeyEvent event, int flags) { final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event); boolean handled = false; if (item != null) { handled = performItemAction(item, flags); } if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) { close(true); } return handled; } @Override public boolean isShortcutKey(int keyCode, KeyEvent event) { return findItemWithShortcutForKey(keyCode, event) != null; } /* * This function will return all the menu and sub-menu items that can * be directly (the shortcut directly corresponds) and indirectly * (the ALT-enabled char corresponds to the shortcut) associated * with the keyCode. */ List<MenuItemImpl> findItemsWithShortcutForKey(int keyCode, KeyEvent event) { final boolean qwerty = isQwertyMode(); final int metaState = event.getMetaState(); final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); // Get the chars associated with the keyCode (i.e using any chording combo) final boolean isKeyCodeMapped = event.getKeyData(possibleChars); // The delete key is not mapped to '\b' so we treat it specially if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) { return null; } Vector<MenuItemImpl> items = new Vector(); // Look for an item whose shortcut is this key. final int N = mItems.size(); for (int i = 0; i < N; i++) { MenuItemImpl item = mItems.get(i); if (item.hasSubMenu()) { List<MenuItemImpl> subMenuItems = ((MenuBuilder)item.getSubMenu()) .findItemsWithShortcutForKey(keyCode, event); items.addAll(subMenuItems); } final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut(); String str = String.valueOf(shortcutChar); if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) && (!"0".equals(str)) && (shortcutChar == possibleChars.meta[0] || shortcutChar == possibleChars.meta[2] || (qwerty && shortcutChar == '\b' && keyCode == KeyEvent.KEYCODE_DEL)) && item.isEnabled()) { items.add(item); } } return items; } /* * We want to return the menu item associated with the key, but if there is no * ambiguity (i.e. there is only one menu item corresponding to the key) we want * to return it even if it's not an exact match; this allow the user to * _not_ use the ALT key for example, making the use of shortcuts slightly more * user-friendly. An example is on the G1, '!' and '1' are on the same key, and * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut). * * On the other hand, if two (or more) shortcuts corresponds to the same key, * we have to only return the exact match. */ MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) { // Get all items that can be associated directly or indirectly with the keyCode List<MenuItemImpl> items = findItemsWithShortcutForKey(keyCode, event); if (items == null) { return null; } final int metaState = event.getMetaState(); final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); // Get the chars associated with the keyCode (i.e using any chording combo) event.getKeyData(possibleChars); // If we have only one element, we can safely returns it if (items.size() == 1) { return items.get(0); } final boolean qwerty = isQwertyMode(); // If we found more than one item associated with the key, // we have to return the exact match for (MenuItemImpl item : items) { final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut(); if ((shortcutChar == possibleChars.meta[0] && (metaState & KeyEvent.META_ALT_ON) == 0) || (shortcutChar == possibleChars.meta[2] && (metaState & KeyEvent.META_ALT_ON) != 0) || (qwerty && shortcutChar == '\b' && keyCode == KeyEvent.KEYCODE_DEL)) { return item; } } return null; } }