2 * Copyright (C) 2006 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.internal.view.menu;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.content.res.Configuration;
26 import android.content.res.Resources;
27 import android.graphics.drawable.Drawable;
28 import android.os.Bundle;
29 import android.os.Parcelable;
30 import android.util.SparseArray;
31 import android.view.ActionProvider;
32 import android.view.ContextMenu.ContextMenuInfo;
33 import android.view.KeyCharacterMap;
34 import android.view.KeyEvent;
35 import android.view.Menu;
36 import android.view.MenuItem;
37 import android.view.SubMenu;
38 import android.view.View;
40 import java.lang.ref.WeakReference;
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.concurrent.CopyOnWriteArrayList;
46 * Implementation of the {@link android.view.Menu} interface for creating a
49 public class MenuBuilder implements Menu {
50 private static final String TAG = "MenuBuilder";
52 private static final String PRESENTER_KEY = "android:menu:presenters";
53 private static final String ACTION_VIEW_STATES_KEY = "android:menu:actionviewstates";
54 private static final String EXPANDED_ACTION_VIEW_ID = "android:menu:expandedactionview";
56 private static final int[] sCategoryToOrder = new int[] {
62 0, /* SELECTED_ALTERNATIVE */
65 private final Context mContext;
66 private final Resources mResources;
69 * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode()
70 * instead of accessing this directly.
72 private boolean mQwertyMode;
75 * Whether the shortcuts should be visible on menus. Use isShortcutsVisible()
76 * instead of accessing this directly.
78 private boolean mShortcutsVisible;
81 * Callback that will receive the various menu-related events generated by
82 * this class. Use getCallback to get a reference to the callback.
84 private Callback mCallback;
86 /** Contains all of the items for this menu */
87 private ArrayList<MenuItemImpl> mItems;
89 /** Contains only the items that are currently visible. This will be created/refreshed from
90 * {@link #getVisibleItems()} */
91 private ArrayList<MenuItemImpl> mVisibleItems;
93 * Whether or not the items (or any one item's shown state) has changed since it was last
94 * fetched from {@link #getVisibleItems()}
96 private boolean mIsVisibleItemsStale;
99 * Contains only the items that should appear in the Action Bar, if present.
101 private ArrayList<MenuItemImpl> mActionItems;
103 * Contains items that should NOT appear in the Action Bar, if present.
105 private ArrayList<MenuItemImpl> mNonActionItems;
108 * Whether or not the items (or any one item's action state) has changed since it was
111 private boolean mIsActionItemsStale;
114 * Default value for how added items should show in the action list.
116 private int mDefaultShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER;
119 * Current use case is Context Menus: As Views populate the context menu, each one has
120 * extra information that should be passed along. This is the current menu info that
121 * should be set on all items added to this menu.
123 private ContextMenuInfo mCurrentMenuInfo;
125 /** Header title for menu types that have a header (context and submenus) */
126 CharSequence mHeaderTitle;
127 /** Header icon for menu types that have a header and support icons (context) */
128 Drawable mHeaderIcon;
129 /** Header custom view for menu types that have a header and support custom views (context) */
133 * Contains the state of the View hierarchy for all menu views when the menu
136 private SparseArray<Parcelable> mFrozenViewStates;
139 * Prevents onItemsChanged from doing its junk, useful for batching commands
140 * that may individually call onItemsChanged.
142 private boolean mPreventDispatchingItemsChanged = false;
143 private boolean mItemsChangedWhileDispatchPrevented = false;
145 private boolean mOptionalIconsVisible = false;
147 private boolean mIsClosing = false;
149 private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>();
151 private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters =
152 new CopyOnWriteArrayList<WeakReference<MenuPresenter>>();
155 * Currently expanded menu item; must be collapsed when we clear.
157 private MenuItemImpl mExpandedItem;
160 * Called by menu to notify of close and selection changes.
162 public interface Callback {
164 * Called when a menu item is selected.
165 * @param menu The menu that is the parent of the item
166 * @param item The menu item that is selected
167 * @return whether the menu item selection was handled
169 public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item);
172 * Called when the mode of the menu changes (for example, from icon to expanded).
174 * @param menu the menu that has changed modes
176 public void onMenuModeChange(MenuBuilder menu);
180 * Called by menu items to execute their associated action
182 public interface ItemInvoker {
183 public boolean invokeItem(MenuItemImpl item);
186 public MenuBuilder(Context context) {
188 mResources = context.getResources();
190 mItems = new ArrayList<MenuItemImpl>();
192 mVisibleItems = new ArrayList<MenuItemImpl>();
193 mIsVisibleItemsStale = true;
195 mActionItems = new ArrayList<MenuItemImpl>();
196 mNonActionItems = new ArrayList<MenuItemImpl>();
197 mIsActionItemsStale = true;
199 setShortcutsVisibleInner(true);
202 public MenuBuilder setDefaultShowAsAction(int defaultShowAsAction) {
203 mDefaultShowAsAction = defaultShowAsAction;
208 * Add a presenter to this menu. This will only hold a WeakReference;
209 * you do not need to explicitly remove a presenter, but you can using
210 * {@link #removeMenuPresenter(MenuPresenter)}.
212 * @param presenter The presenter to add
214 public void addMenuPresenter(MenuPresenter presenter) {
215 mPresenters.add(new WeakReference<MenuPresenter>(presenter));
216 presenter.initForMenu(mContext, this);
217 mIsActionItemsStale = true;
221 * Remove a presenter from this menu. That presenter will no longer
222 * receive notifications of updates to this menu's data.
224 * @param presenter The presenter to remove
226 public void removeMenuPresenter(MenuPresenter presenter) {
227 for (WeakReference<MenuPresenter> ref : mPresenters) {
228 final MenuPresenter item = ref.get();
229 if (item == null || item == presenter) {
230 mPresenters.remove(ref);
235 private void dispatchPresenterUpdate(boolean cleared) {
236 if (mPresenters.isEmpty()) return;
238 stopDispatchingItemsChanged();
239 for (WeakReference<MenuPresenter> ref : mPresenters) {
240 final MenuPresenter presenter = ref.get();
241 if (presenter == null) {
242 mPresenters.remove(ref);
244 presenter.updateMenuView(cleared);
247 startDispatchingItemsChanged();
250 private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu,
251 MenuPresenter preferredPresenter) {
252 if (mPresenters.isEmpty()) return false;
254 boolean result = false;
256 // Try the preferred presenter first.
257 if (preferredPresenter != null) {
258 result = preferredPresenter.onSubMenuSelected(subMenu);
261 for (WeakReference<MenuPresenter> ref : mPresenters) {
262 final MenuPresenter presenter = ref.get();
263 if (presenter == null) {
264 mPresenters.remove(ref);
265 } else if (!result) {
266 result = presenter.onSubMenuSelected(subMenu);
272 private void dispatchSaveInstanceState(Bundle outState) {
273 if (mPresenters.isEmpty()) return;
275 SparseArray<Parcelable> presenterStates = new SparseArray<Parcelable>();
277 for (WeakReference<MenuPresenter> ref : mPresenters) {
278 final MenuPresenter presenter = ref.get();
279 if (presenter == null) {
280 mPresenters.remove(ref);
282 final int id = presenter.getId();
284 final Parcelable state = presenter.onSaveInstanceState();
286 presenterStates.put(id, state);
292 outState.putSparseParcelableArray(PRESENTER_KEY, presenterStates);
295 private void dispatchRestoreInstanceState(Bundle state) {
296 SparseArray<Parcelable> presenterStates = state.getSparseParcelableArray(PRESENTER_KEY);
298 if (presenterStates == null || mPresenters.isEmpty()) return;
300 for (WeakReference<MenuPresenter> ref : mPresenters) {
301 final MenuPresenter presenter = ref.get();
302 if (presenter == null) {
303 mPresenters.remove(ref);
305 final int id = presenter.getId();
307 Parcelable parcel = presenterStates.get(id);
308 if (parcel != null) {
309 presenter.onRestoreInstanceState(parcel);
316 public void savePresenterStates(Bundle outState) {
317 dispatchSaveInstanceState(outState);
320 public void restorePresenterStates(Bundle state) {
321 dispatchRestoreInstanceState(state);
324 public void saveActionViewStates(Bundle outStates) {
325 SparseArray<Parcelable> viewStates = null;
327 final int itemCount = size();
328 for (int i = 0; i < itemCount; i++) {
329 final MenuItem item = getItem(i);
330 final View v = item.getActionView();
331 if (v != null && v.getId() != View.NO_ID) {
332 if (viewStates == null) {
333 viewStates = new SparseArray<Parcelable>();
335 v.saveHierarchyState(viewStates);
336 if (item.isActionViewExpanded()) {
337 outStates.putInt(EXPANDED_ACTION_VIEW_ID, item.getItemId());
340 if (item.hasSubMenu()) {
341 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
342 subMenu.saveActionViewStates(outStates);
346 if (viewStates != null) {
347 outStates.putSparseParcelableArray(getActionViewStatesKey(), viewStates);
351 public void restoreActionViewStates(Bundle states) {
352 if (states == null) {
356 SparseArray<Parcelable> viewStates = states.getSparseParcelableArray(
357 getActionViewStatesKey());
359 final int itemCount = size();
360 for (int i = 0; i < itemCount; i++) {
361 final MenuItem item = getItem(i);
362 final View v = item.getActionView();
363 if (v != null && v.getId() != View.NO_ID) {
364 v.restoreHierarchyState(viewStates);
366 if (item.hasSubMenu()) {
367 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
368 subMenu.restoreActionViewStates(states);
372 final int expandedId = states.getInt(EXPANDED_ACTION_VIEW_ID);
373 if (expandedId > 0) {
374 MenuItem itemToExpand = findItem(expandedId);
375 if (itemToExpand != null) {
376 itemToExpand.expandActionView();
381 protected String getActionViewStatesKey() {
382 return ACTION_VIEW_STATES_KEY;
385 public void setCallback(Callback cb) {
390 * Adds an item to the menu. The other add methods funnel to this.
392 private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
393 final int ordering = getOrdering(categoryOrder);
395 final MenuItemImpl item = new MenuItemImpl(this, group, id, categoryOrder,
396 ordering, title, mDefaultShowAsAction);
398 if (mCurrentMenuInfo != null) {
399 // Pass along the current menu info
400 item.setMenuInfo(mCurrentMenuInfo);
403 mItems.add(findInsertIndex(mItems, ordering), item);
404 onItemsChanged(true);
409 public MenuItem add(CharSequence title) {
410 return addInternal(0, 0, 0, title);
413 public MenuItem add(int titleRes) {
414 return addInternal(0, 0, 0, mResources.getString(titleRes));
417 public MenuItem add(int group, int id, int categoryOrder, CharSequence title) {
418 return addInternal(group, id, categoryOrder, title);
421 public MenuItem add(int group, int id, int categoryOrder, int title) {
422 return addInternal(group, id, categoryOrder, mResources.getString(title));
425 public SubMenu addSubMenu(CharSequence title) {
426 return addSubMenu(0, 0, 0, title);
429 public SubMenu addSubMenu(int titleRes) {
430 return addSubMenu(0, 0, 0, mResources.getString(titleRes));
433 public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
434 final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title);
435 final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item);
436 item.setSubMenu(subMenu);
441 public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) {
442 return addSubMenu(group, id, categoryOrder, mResources.getString(title));
445 public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller,
446 Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) {
447 PackageManager pm = mContext.getPackageManager();
448 final List<ResolveInfo> lri =
449 pm.queryIntentActivityOptions(caller, specifics, intent, 0);
450 final int N = lri != null ? lri.size() : 0;
452 if ((flags & FLAG_APPEND_TO_GROUP) == 0) {
456 for (int i=0; i<N; i++) {
457 final ResolveInfo ri = lri.get(i);
458 Intent rintent = new Intent(
459 ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]);
460 rintent.setComponent(new ComponentName(
461 ri.activityInfo.applicationInfo.packageName,
462 ri.activityInfo.name));
463 final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm))
464 .setIcon(ri.loadIcon(pm))
466 if (outSpecificItems != null && ri.specificIndex >= 0) {
467 outSpecificItems[ri.specificIndex] = item;
474 public void removeItem(int id) {
475 removeItemAtInt(findItemIndex(id), true);
478 public void removeGroup(int group) {
479 final int i = findGroupIndex(group);
482 final int maxRemovable = mItems.size() - i;
484 while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) {
485 // Don't force update for each one, this method will do it at the end
486 removeItemAtInt(i, false);
490 onItemsChanged(true);
495 * Remove the item at the given index and optionally forces menu views to
498 * @param index The index of the item to be removed. If this index is
499 * invalid an exception is thrown.
500 * @param updateChildrenOnMenuViews Whether to force update on menu views.
501 * Please make sure you eventually call this after your batch of
504 private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) {
505 if ((index < 0) || (index >= mItems.size())) return;
507 mItems.remove(index);
509 if (updateChildrenOnMenuViews) onItemsChanged(true);
512 public void removeItemAt(int index) {
513 removeItemAtInt(index, true);
516 public void clearAll() {
517 mPreventDispatchingItemsChanged = true;
520 mPreventDispatchingItemsChanged = false;
521 mItemsChangedWhileDispatchPrevented = false;
522 onItemsChanged(true);
525 public void clear() {
526 if (mExpandedItem != null) {
527 collapseItemActionView(mExpandedItem);
531 onItemsChanged(true);
534 void setExclusiveItemChecked(MenuItem item) {
535 final int group = item.getGroupId();
537 final int N = mItems.size();
538 for (int i = 0; i < N; i++) {
539 MenuItemImpl curItem = mItems.get(i);
540 if (curItem.getGroupId() == group) {
541 if (!curItem.isExclusiveCheckable()) continue;
542 if (!curItem.isCheckable()) continue;
544 // Check the item meant to be checked, uncheck the others (that are in the group)
545 curItem.setCheckedInt(curItem == item);
550 public void setGroupCheckable(int group, boolean checkable, boolean exclusive) {
551 final int N = mItems.size();
553 for (int i = 0; i < N; i++) {
554 MenuItemImpl item = mItems.get(i);
555 if (item.getGroupId() == group) {
556 item.setExclusiveCheckable(exclusive);
557 item.setCheckable(checkable);
562 public void setGroupVisible(int group, boolean visible) {
563 final int N = mItems.size();
565 // We handle the notification of items being changed ourselves, so we use setVisibleInt rather
566 // than setVisible and at the end notify of items being changed
568 boolean changedAtLeastOneItem = false;
569 for (int i = 0; i < N; i++) {
570 MenuItemImpl item = mItems.get(i);
571 if (item.getGroupId() == group) {
572 if (item.setVisibleInt(visible)) changedAtLeastOneItem = true;
576 if (changedAtLeastOneItem) onItemsChanged(true);
579 public void setGroupEnabled(int group, boolean enabled) {
580 final int N = mItems.size();
582 for (int i = 0; i < N; i++) {
583 MenuItemImpl item = mItems.get(i);
584 if (item.getGroupId() == group) {
585 item.setEnabled(enabled);
590 public boolean hasVisibleItems() {
591 final int size = size();
593 for (int i = 0; i < size; i++) {
594 MenuItemImpl item = mItems.get(i);
595 if (item.isVisible()) {
603 public MenuItem findItem(int id) {
604 final int size = size();
605 for (int i = 0; i < size; i++) {
606 MenuItemImpl item = mItems.get(i);
607 if (item.getItemId() == id) {
609 } else if (item.hasSubMenu()) {
610 MenuItem possibleItem = item.getSubMenu().findItem(id);
612 if (possibleItem != null) {
621 public int findItemIndex(int id) {
622 final int size = size();
624 for (int i = 0; i < size; i++) {
625 MenuItemImpl item = mItems.get(i);
626 if (item.getItemId() == id) {
634 public int findGroupIndex(int group) {
635 return findGroupIndex(group, 0);
638 public int findGroupIndex(int group, int start) {
639 final int size = size();
645 for (int i = start; i < size; i++) {
646 final MenuItemImpl item = mItems.get(i);
648 if (item.getGroupId() == group) {
657 return mItems.size();
661 public MenuItem getItem(int index) {
662 return mItems.get(index);
665 public boolean isShortcutKey(int keyCode, KeyEvent event) {
666 return findItemWithShortcutForKey(keyCode, event) != null;
669 public void setQwertyMode(boolean isQwerty) {
670 mQwertyMode = isQwerty;
672 onItemsChanged(false);
676 * Returns the ordering across all items. This will grab the category from
677 * the upper bits, find out how to order the category with respect to other
678 * categories, and combine it with the lower bits.
680 * @param categoryOrder The category order for a particular item (if it has
681 * not been or/add with a category, the default category is
683 * @return An ordering integer that can be used to order this item across
684 * all the items (even from other categories).
686 private static int getOrdering(int categoryOrder) {
687 final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT;
689 if (index < 0 || index >= sCategoryToOrder.length) {
690 throw new IllegalArgumentException("order does not contain a valid category.");
693 return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK);
697 * @return whether the menu shortcuts are in qwerty mode or not
699 boolean isQwertyMode() {
704 * Sets whether the shortcuts should be visible on menus. Devices without hardware
705 * key input will never make shortcuts visible even if this method is passed 'true'.
707 * @param shortcutsVisible Whether shortcuts should be visible (if true and a
708 * menu item does not have a shortcut defined, that item will
709 * still NOT show a shortcut)
711 public void setShortcutsVisible(boolean shortcutsVisible) {
712 if (mShortcutsVisible == shortcutsVisible) return;
714 setShortcutsVisibleInner(shortcutsVisible);
715 onItemsChanged(false);
718 private void setShortcutsVisibleInner(boolean shortcutsVisible) {
719 mShortcutsVisible = shortcutsVisible
720 && mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS
721 && mResources.getBoolean(
722 com.android.internal.R.bool.config_showMenuShortcutsWhenKeyboardPresent);
726 * @return Whether shortcuts should be visible on menus.
728 public boolean isShortcutsVisible() {
729 return mShortcutsVisible;
732 Resources getResources() {
736 public Context getContext() {
740 boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) {
741 return mCallback != null && mCallback.onMenuItemSelected(menu, item);
745 * Dispatch a mode change event to this menu's callback.
747 public void changeMenuMode() {
748 if (mCallback != null) {
749 mCallback.onMenuModeChange(this);
753 private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) {
754 for (int i = items.size() - 1; i >= 0; i--) {
755 MenuItemImpl item = items.get(i);
756 if (item.getOrdering() <= ordering) {
764 public boolean performShortcut(int keyCode, KeyEvent event, int flags) {
765 final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event);
767 boolean handled = false;
770 handled = performItemAction(item, flags);
773 if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) {
781 * This function will return all the menu and sub-menu items that can
782 * be directly (the shortcut directly corresponds) and indirectly
783 * (the ALT-enabled char corresponds to the shortcut) associated
786 void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) {
787 final boolean qwerty = isQwertyMode();
788 final int metaState = event.getMetaState();
789 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
790 // Get the chars associated with the keyCode (i.e using any chording combo)
791 final boolean isKeyCodeMapped = event.getKeyData(possibleChars);
792 // The delete key is not mapped to '\b' so we treat it specially
793 if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) {
797 // Look for an item whose shortcut is this key.
798 final int N = mItems.size();
799 for (int i = 0; i < N; i++) {
800 MenuItemImpl item = mItems.get(i);
801 if (item.hasSubMenu()) {
802 ((MenuBuilder)item.getSubMenu()).findItemsWithShortcutForKey(items, keyCode, event);
804 final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut();
805 if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) &&
806 (shortcutChar != 0) &&
807 (shortcutChar == possibleChars.meta[0]
808 || shortcutChar == possibleChars.meta[2]
809 || (qwerty && shortcutChar == '\b' &&
810 keyCode == KeyEvent.KEYCODE_DEL)) &&
818 * We want to return the menu item associated with the key, but if there is no
819 * ambiguity (i.e. there is only one menu item corresponding to the key) we want
820 * to return it even if it's not an exact match; this allow the user to
821 * _not_ use the ALT key for example, making the use of shortcuts slightly more
822 * user-friendly. An example is on the G1, '!' and '1' are on the same key, and
823 * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut).
825 * On the other hand, if two (or more) shortcuts corresponds to the same key,
826 * we have to only return the exact match.
828 MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) {
829 // Get all items that can be associated directly or indirectly with the keyCode
830 ArrayList<MenuItemImpl> items = mTempShortcutItemList;
832 findItemsWithShortcutForKey(items, keyCode, event);
834 if (items.isEmpty()) {
838 final int metaState = event.getMetaState();
839 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
840 // Get the chars associated with the keyCode (i.e using any chording combo)
841 event.getKeyData(possibleChars);
843 // If we have only one element, we can safely returns it
844 final int size = items.size();
849 final boolean qwerty = isQwertyMode();
850 // If we found more than one item associated with the key,
851 // we have to return the exact match
852 for (int i = 0; i < size; i++) {
853 final MenuItemImpl item = items.get(i);
854 final char shortcutChar = qwerty ? item.getAlphabeticShortcut() :
855 item.getNumericShortcut();
856 if ((shortcutChar == possibleChars.meta[0] &&
857 (metaState & KeyEvent.META_ALT_ON) == 0)
858 || (shortcutChar == possibleChars.meta[2] &&
859 (metaState & KeyEvent.META_ALT_ON) != 0)
860 || (qwerty && shortcutChar == '\b' &&
861 keyCode == KeyEvent.KEYCODE_DEL)) {
868 public boolean performIdentifierAction(int id, int flags) {
869 // Look for an item whose identifier is the id.
870 return performItemAction(findItem(id), flags);
873 public boolean performItemAction(MenuItem item, int flags) {
874 return performItemAction(item, null, flags);
877 public boolean performItemAction(MenuItem item, MenuPresenter preferredPresenter, int flags) {
878 MenuItemImpl itemImpl = (MenuItemImpl) item;
880 if (itemImpl == null || !itemImpl.isEnabled()) {
884 boolean invoked = itemImpl.invoke();
886 final ActionProvider provider = item.getActionProvider();
887 final boolean providerHasSubMenu = provider != null && provider.hasSubMenu();
888 if (itemImpl.hasCollapsibleActionView()) {
889 invoked |= itemImpl.expandActionView();
890 if (invoked) close(true);
891 } else if (itemImpl.hasSubMenu() || providerHasSubMenu) {
894 if (!itemImpl.hasSubMenu()) {
895 itemImpl.setSubMenu(new SubMenuBuilder(getContext(), this, itemImpl));
898 final SubMenuBuilder subMenu = (SubMenuBuilder) itemImpl.getSubMenu();
899 if (providerHasSubMenu) {
900 provider.onPrepareSubMenu(subMenu);
902 invoked |= dispatchSubMenuSelected(subMenu, preferredPresenter);
903 if (!invoked) close(true);
905 if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) {
914 * Closes the visible menu.
916 * @param allMenusAreClosing Whether the menus are completely closing (true),
917 * or whether there is another menu coming in this menu's place
918 * (false). For example, if the menu is closing because a
919 * sub menu is about to be shown, <var>allMenusAreClosing</var>
922 final void close(boolean allMenusAreClosing) {
923 if (mIsClosing) return;
926 for (WeakReference<MenuPresenter> ref : mPresenters) {
927 final MenuPresenter presenter = ref.get();
928 if (presenter == null) {
929 mPresenters.remove(ref);
931 presenter.onCloseMenu(this, allMenusAreClosing);
938 public void close() {
943 * Called when an item is added or removed.
945 * @param structureChanged true if the menu structure changed,
946 * false if only item properties changed.
947 * (Visibility is a structural property since it affects layout.)
949 void onItemsChanged(boolean structureChanged) {
950 if (!mPreventDispatchingItemsChanged) {
951 if (structureChanged) {
952 mIsVisibleItemsStale = true;
953 mIsActionItemsStale = true;
956 dispatchPresenterUpdate(structureChanged);
958 mItemsChangedWhileDispatchPrevented = true;
963 * Stop dispatching item changed events to presenters until
964 * {@link #startDispatchingItemsChanged()} is called. Useful when
965 * many menu operations are going to be performed as a batch.
967 public void stopDispatchingItemsChanged() {
968 if (!mPreventDispatchingItemsChanged) {
969 mPreventDispatchingItemsChanged = true;
970 mItemsChangedWhileDispatchPrevented = false;
974 public void startDispatchingItemsChanged() {
975 mPreventDispatchingItemsChanged = false;
977 if (mItemsChangedWhileDispatchPrevented) {
978 mItemsChangedWhileDispatchPrevented = false;
979 onItemsChanged(true);
984 * Called by {@link MenuItemImpl} when its visible flag is changed.
985 * @param item The item that has gone through a visibility change.
987 void onItemVisibleChanged(MenuItemImpl item) {
988 // Notify of items being changed
989 mIsVisibleItemsStale = true;
990 onItemsChanged(true);
994 * Called by {@link MenuItemImpl} when its action request status is changed.
995 * @param item The item that has gone through a change in action request status.
997 void onItemActionRequestChanged(MenuItemImpl item) {
998 // Notify of items being changed
999 mIsActionItemsStale = true;
1000 onItemsChanged(true);
1003 ArrayList<MenuItemImpl> getVisibleItems() {
1004 if (!mIsVisibleItemsStale) return mVisibleItems;
1006 // Refresh the visible items
1007 mVisibleItems.clear();
1009 final int itemsSize = mItems.size();
1011 for (int i = 0; i < itemsSize; i++) {
1012 item = mItems.get(i);
1013 if (item.isVisible()) mVisibleItems.add(item);
1016 mIsVisibleItemsStale = false;
1017 mIsActionItemsStale = true;
1019 return mVisibleItems;
1023 * This method determines which menu items get to be 'action items' that will appear
1024 * in an action bar and which items should be 'overflow items' in a secondary menu.
1025 * The rules are as follows:
1027 * <p>Items are considered for inclusion in the order specified within the menu.
1028 * There is a limit of mMaxActionItems as a total count, optionally including the overflow
1029 * menu button itself. This is a soft limit; if an item shares a group ID with an item
1030 * previously included as an action item, the new item will stay with its group and become
1031 * an action item itself even if it breaks the max item count limit. This is done to
1032 * limit the conceptual complexity of the items presented within an action bar. Only a few
1033 * unrelated concepts should be presented to the user in this space, and groups are treated
1034 * as a single concept.
1036 * <p>There is also a hard limit of consumed measurable space: mActionWidthLimit. This
1037 * limit may be broken by a single item that exceeds the remaining space, but no further
1038 * items may be added. If an item that is part of a group cannot fit within the remaining
1039 * measured width, the entire group will be demoted to overflow. This is done to ensure room
1040 * for navigation and other affordances in the action bar as well as reduce general UI clutter.
1042 * <p>The space freed by demoting a full group cannot be consumed by future menu items.
1043 * Once items begin to overflow, all future items become overflow items as well. This is
1044 * to avoid inadvertent reordering that may break the app's intended design.
1046 public void flagActionItems() {
1047 // Important side effect: if getVisibleItems is stale it may refresh,
1048 // which can affect action items staleness.
1049 final ArrayList<MenuItemImpl> visibleItems = getVisibleItems();
1051 if (!mIsActionItemsStale) {
1055 // Presenters flag action items as needed.
1056 boolean flagged = false;
1057 for (WeakReference<MenuPresenter> ref : mPresenters) {
1058 final MenuPresenter presenter = ref.get();
1059 if (presenter == null) {
1060 mPresenters.remove(ref);
1062 flagged |= presenter.flagActionItems();
1067 mActionItems.clear();
1068 mNonActionItems.clear();
1069 final int itemsSize = visibleItems.size();
1070 for (int i = 0; i < itemsSize; i++) {
1071 MenuItemImpl item = visibleItems.get(i);
1072 if (item.isActionButton()) {
1073 mActionItems.add(item);
1075 mNonActionItems.add(item);
1079 // Nobody flagged anything, everything is a non-action item.
1080 // (This happens during a first pass with no action-item presenters.)
1081 mActionItems.clear();
1082 mNonActionItems.clear();
1083 mNonActionItems.addAll(getVisibleItems());
1085 mIsActionItemsStale = false;
1088 ArrayList<MenuItemImpl> getActionItems() {
1090 return mActionItems;
1093 ArrayList<MenuItemImpl> getNonActionItems() {
1095 return mNonActionItems;
1098 public void clearHeader() {
1100 mHeaderTitle = null;
1103 onItemsChanged(false);
1106 private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes,
1107 final Drawable icon, final View view) {
1108 final Resources r = getResources();
1113 // If using a custom view, then the title and icon aren't used
1114 mHeaderTitle = null;
1118 mHeaderTitle = r.getText(titleRes);
1119 } else if (title != null) {
1120 mHeaderTitle = title;
1124 mHeaderIcon = r.getDrawable(iconRes);
1125 } else if (icon != null) {
1129 // If using the title or icon, then a custom view isn't used
1134 onItemsChanged(false);
1138 * Sets the header's title. This replaces the header view. Called by the
1139 * builder-style methods of subclasses.
1141 * @param title The new title.
1142 * @return This MenuBuilder so additional setters can be called.
1144 protected MenuBuilder setHeaderTitleInt(CharSequence title) {
1145 setHeaderInternal(0, title, 0, null, null);
1150 * Sets the header's title. This replaces the header view. Called by the
1151 * builder-style methods of subclasses.
1153 * @param titleRes The new title (as a resource ID).
1154 * @return This MenuBuilder so additional setters can be called.
1156 protected MenuBuilder setHeaderTitleInt(int titleRes) {
1157 setHeaderInternal(titleRes, null, 0, null, null);
1162 * Sets the header's icon. This replaces the header view. Called by the
1163 * builder-style methods of subclasses.
1165 * @param icon The new icon.
1166 * @return This MenuBuilder so additional setters can be called.
1168 protected MenuBuilder setHeaderIconInt(Drawable icon) {
1169 setHeaderInternal(0, null, 0, icon, null);
1174 * Sets the header's icon. This replaces the header view. Called by the
1175 * builder-style methods of subclasses.
1177 * @param iconRes The new icon (as a resource ID).
1178 * @return This MenuBuilder so additional setters can be called.
1180 protected MenuBuilder setHeaderIconInt(int iconRes) {
1181 setHeaderInternal(0, null, iconRes, null, null);
1186 * Sets the header's view. This replaces the title and icon. Called by the
1187 * builder-style methods of subclasses.
1189 * @param view The new view.
1190 * @return This MenuBuilder so additional setters can be called.
1192 protected MenuBuilder setHeaderViewInt(View view) {
1193 setHeaderInternal(0, null, 0, null, view);
1197 public CharSequence getHeaderTitle() {
1198 return mHeaderTitle;
1201 public Drawable getHeaderIcon() {
1205 public View getHeaderView() {
1210 * Gets the root menu (if this is a submenu, find its root menu).
1211 * @return The root menu.
1213 public MenuBuilder getRootMenu() {
1218 * Sets the current menu info that is set on all items added to this menu
1219 * (until this is called again with different menu info, in which case that
1220 * one will be added to all subsequent item additions).
1222 * @param menuInfo The extra menu information to add.
1224 public void setCurrentMenuInfo(ContextMenuInfo menuInfo) {
1225 mCurrentMenuInfo = menuInfo;
1228 void setOptionalIconsVisible(boolean visible) {
1229 mOptionalIconsVisible = visible;
1232 boolean getOptionalIconsVisible() {
1233 return mOptionalIconsVisible;
1236 public boolean expandItemActionView(MenuItemImpl item) {
1237 if (mPresenters.isEmpty()) return false;
1239 boolean expanded = false;
1241 stopDispatchingItemsChanged();
1242 for (WeakReference<MenuPresenter> ref : mPresenters) {
1243 final MenuPresenter presenter = ref.get();
1244 if (presenter == null) {
1245 mPresenters.remove(ref);
1246 } else if ((expanded = presenter.expandItemActionView(this, item))) {
1250 startDispatchingItemsChanged();
1253 mExpandedItem = item;
1258 public boolean collapseItemActionView(MenuItemImpl item) {
1259 if (mPresenters.isEmpty() || mExpandedItem != item) return false;
1261 boolean collapsed = false;
1263 stopDispatchingItemsChanged();
1264 for (WeakReference<MenuPresenter> ref : mPresenters) {
1265 final MenuPresenter presenter = ref.get();
1266 if (presenter == null) {
1267 mPresenters.remove(ref);
1268 } else if ((collapsed = presenter.collapseItemActionView(this, item))) {
1272 startDispatchingItemsChanged();
1275 mExpandedItem = null;
1280 public MenuItemImpl getExpandedItem() {
1281 return mExpandedItem;