Home | History | Annotate | Download | only in menu
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      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
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     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.
     15  */
     16 
     17 package com.android.internal.view.menu;
     18 
     19 
     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;
     39 
     40 import java.lang.ref.WeakReference;
     41 import java.util.ArrayList;
     42 import java.util.List;
     43 import java.util.concurrent.CopyOnWriteArrayList;
     44 
     45 /**
     46  * Implementation of the {@link android.view.Menu} interface for creating a
     47  * standard menu UI.
     48  */
     49 public class MenuBuilder implements Menu {
     50     private static final String TAG = "MenuBuilder";
     51 
     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";
     55 
     56     private static final int[]  sCategoryToOrder = new int[] {
     57         1, /* No category */
     58         4, /* CONTAINER */
     59         5, /* SYSTEM */
     60         3, /* SECONDARY */
     61         2, /* ALTERNATIVE */
     62         0, /* SELECTED_ALTERNATIVE */
     63     };
     64 
     65     private final Context mContext;
     66     private final Resources mResources;
     67 
     68     /**
     69      * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode()
     70      * instead of accessing this directly.
     71      */
     72     private boolean mQwertyMode;
     73 
     74     /**
     75      * Whether the shortcuts should be visible on menus. Use isShortcutsVisible()
     76      * instead of accessing this directly.
     77      */
     78     private boolean mShortcutsVisible;
     79 
     80     /**
     81      * Callback that will receive the various menu-related events generated by
     82      * this class. Use getCallback to get a reference to the callback.
     83      */
     84     private Callback mCallback;
     85 
     86     /** Contains all of the items for this menu */
     87     private ArrayList<MenuItemImpl> mItems;
     88 
     89     /** Contains only the items that are currently visible.  This will be created/refreshed from
     90      * {@link #getVisibleItems()} */
     91     private ArrayList<MenuItemImpl> mVisibleItems;
     92     /**
     93      * Whether or not the items (or any one item's shown state) has changed since it was last
     94      * fetched from {@link #getVisibleItems()}
     95      */
     96     private boolean mIsVisibleItemsStale;
     97 
     98     /**
     99      * Contains only the items that should appear in the Action Bar, if present.
    100      */
    101     private ArrayList<MenuItemImpl> mActionItems;
    102     /**
    103      * Contains items that should NOT appear in the Action Bar, if present.
    104      */
    105     private ArrayList<MenuItemImpl> mNonActionItems;
    106 
    107     /**
    108      * Whether or not the items (or any one item's action state) has changed since it was
    109      * last fetched.
    110      */
    111     private boolean mIsActionItemsStale;
    112 
    113     /**
    114      * Default value for how added items should show in the action list.
    115      */
    116     private int mDefaultShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER;
    117 
    118     /**
    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.
    122      */
    123     private ContextMenuInfo mCurrentMenuInfo;
    124 
    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) */
    130     View mHeaderView;
    131 
    132     /**
    133      * Contains the state of the View hierarchy for all menu views when the menu
    134      * was frozen.
    135      */
    136     private SparseArray<Parcelable> mFrozenViewStates;
    137 
    138     /**
    139      * Prevents onItemsChanged from doing its junk, useful for batching commands
    140      * that may individually call onItemsChanged.
    141      */
    142     private boolean mPreventDispatchingItemsChanged = false;
    143     private boolean mItemsChangedWhileDispatchPrevented = false;
    144 
    145     private boolean mOptionalIconsVisible = false;
    146 
    147     private boolean mIsClosing = false;
    148 
    149     private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>();
    150 
    151     private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters =
    152             new CopyOnWriteArrayList<WeakReference<MenuPresenter>>();
    153 
    154     /**
    155      * Currently expanded menu item; must be collapsed when we clear.
    156      */
    157     private MenuItemImpl mExpandedItem;
    158 
    159     /**
    160      * Called by menu to notify of close and selection changes.
    161      */
    162     public interface Callback {
    163         /**
    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
    168          */
    169         public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item);
    170 
    171         /**
    172          * Called when the mode of the menu changes (for example, from icon to expanded).
    173          *
    174          * @param menu the menu that has changed modes
    175          */
    176         public void onMenuModeChange(MenuBuilder menu);
    177     }
    178 
    179     /**
    180      * Called by menu items to execute their associated action
    181      */
    182     public interface ItemInvoker {
    183         public boolean invokeItem(MenuItemImpl item);
    184     }
    185 
    186     public MenuBuilder(Context context) {
    187         mContext = context;
    188         mResources = context.getResources();
    189 
    190         mItems = new ArrayList<MenuItemImpl>();
    191 
    192         mVisibleItems = new ArrayList<MenuItemImpl>();
    193         mIsVisibleItemsStale = true;
    194 
    195         mActionItems = new ArrayList<MenuItemImpl>();
    196         mNonActionItems = new ArrayList<MenuItemImpl>();
    197         mIsActionItemsStale = true;
    198 
    199         setShortcutsVisibleInner(true);
    200     }
    201 
    202     public MenuBuilder setDefaultShowAsAction(int defaultShowAsAction) {
    203         mDefaultShowAsAction = defaultShowAsAction;
    204         return this;
    205     }
    206 
    207     /**
    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)}.
    211      *
    212      * @param presenter The presenter to add
    213      */
    214     public void addMenuPresenter(MenuPresenter presenter) {
    215         mPresenters.add(new WeakReference<MenuPresenter>(presenter));
    216         presenter.initForMenu(mContext, this);
    217         mIsActionItemsStale = true;
    218     }
    219 
    220     /**
    221      * Remove a presenter from this menu. That presenter will no longer
    222      * receive notifications of updates to this menu's data.
    223      *
    224      * @param presenter The presenter to remove
    225      */
    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);
    231             }
    232         }
    233     }
    234 
    235     private void dispatchPresenterUpdate(boolean cleared) {
    236         if (mPresenters.isEmpty()) return;
    237 
    238         stopDispatchingItemsChanged();
    239         for (WeakReference<MenuPresenter> ref : mPresenters) {
    240             final MenuPresenter presenter = ref.get();
    241             if (presenter == null) {
    242                 mPresenters.remove(ref);
    243             } else {
    244                 presenter.updateMenuView(cleared);
    245             }
    246         }
    247         startDispatchingItemsChanged();
    248     }
    249 
    250     private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu) {
    251         if (mPresenters.isEmpty()) return false;
    252 
    253         boolean result = false;
    254 
    255         for (WeakReference<MenuPresenter> ref : mPresenters) {
    256             final MenuPresenter presenter = ref.get();
    257             if (presenter == null) {
    258                 mPresenters.remove(ref);
    259             } else if (!result) {
    260                 result = presenter.onSubMenuSelected(subMenu);
    261             }
    262         }
    263         return result;
    264     }
    265 
    266     private void dispatchSaveInstanceState(Bundle outState) {
    267         if (mPresenters.isEmpty()) return;
    268 
    269         SparseArray<Parcelable> presenterStates = new SparseArray<Parcelable>();
    270 
    271         for (WeakReference<MenuPresenter> ref : mPresenters) {
    272             final MenuPresenter presenter = ref.get();
    273             if (presenter == null) {
    274                 mPresenters.remove(ref);
    275             } else {
    276                 final int id = presenter.getId();
    277                 if (id > 0) {
    278                     final Parcelable state = presenter.onSaveInstanceState();
    279                     if (state != null) {
    280                         presenterStates.put(id, state);
    281                     }
    282                 }
    283             }
    284         }
    285 
    286         outState.putSparseParcelableArray(PRESENTER_KEY, presenterStates);
    287     }
    288 
    289     private void dispatchRestoreInstanceState(Bundle state) {
    290         SparseArray<Parcelable> presenterStates = state.getSparseParcelableArray(PRESENTER_KEY);
    291 
    292         if (presenterStates == null || mPresenters.isEmpty()) return;
    293 
    294         for (WeakReference<MenuPresenter> ref : mPresenters) {
    295             final MenuPresenter presenter = ref.get();
    296             if (presenter == null) {
    297                 mPresenters.remove(ref);
    298             } else {
    299                 final int id = presenter.getId();
    300                 if (id > 0) {
    301                     Parcelable parcel = presenterStates.get(id);
    302                     if (parcel != null) {
    303                         presenter.onRestoreInstanceState(parcel);
    304                     }
    305                 }
    306             }
    307         }
    308     }
    309 
    310     public void savePresenterStates(Bundle outState) {
    311         dispatchSaveInstanceState(outState);
    312     }
    313 
    314     public void restorePresenterStates(Bundle state) {
    315         dispatchRestoreInstanceState(state);
    316     }
    317 
    318     public void saveActionViewStates(Bundle outStates) {
    319         SparseArray<Parcelable> viewStates = null;
    320 
    321         final int itemCount = size();
    322         for (int i = 0; i < itemCount; i++) {
    323             final MenuItem item = getItem(i);
    324             final View v = item.getActionView();
    325             if (v != null && v.getId() != View.NO_ID) {
    326                 if (viewStates == null) {
    327                     viewStates = new SparseArray<Parcelable>();
    328                 }
    329                 v.saveHierarchyState(viewStates);
    330                 if (item.isActionViewExpanded()) {
    331                     outStates.putInt(EXPANDED_ACTION_VIEW_ID, item.getItemId());
    332                 }
    333             }
    334             if (item.hasSubMenu()) {
    335                 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
    336                 subMenu.saveActionViewStates(outStates);
    337             }
    338         }
    339 
    340         if (viewStates != null) {
    341             outStates.putSparseParcelableArray(getActionViewStatesKey(), viewStates);
    342         }
    343     }
    344 
    345     public void restoreActionViewStates(Bundle states) {
    346         if (states == null) {
    347             return;
    348         }
    349 
    350         SparseArray<Parcelable> viewStates = states.getSparseParcelableArray(
    351                 getActionViewStatesKey());
    352 
    353         final int itemCount = size();
    354         for (int i = 0; i < itemCount; i++) {
    355             final MenuItem item = getItem(i);
    356             final View v = item.getActionView();
    357             if (v != null && v.getId() != View.NO_ID) {
    358                 v.restoreHierarchyState(viewStates);
    359             }
    360             if (item.hasSubMenu()) {
    361                 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
    362                 subMenu.restoreActionViewStates(states);
    363             }
    364         }
    365 
    366         final int expandedId = states.getInt(EXPANDED_ACTION_VIEW_ID);
    367         if (expandedId > 0) {
    368             MenuItem itemToExpand = findItem(expandedId);
    369             if (itemToExpand != null) {
    370                 itemToExpand.expandActionView();
    371             }
    372         }
    373     }
    374 
    375     protected String getActionViewStatesKey() {
    376         return ACTION_VIEW_STATES_KEY;
    377     }
    378 
    379     public void setCallback(Callback cb) {
    380         mCallback = cb;
    381     }
    382 
    383     /**
    384      * Adds an item to the menu.  The other add methods funnel to this.
    385      */
    386     private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
    387         final int ordering = getOrdering(categoryOrder);
    388 
    389         final MenuItemImpl item = new MenuItemImpl(this, group, id, categoryOrder,
    390                 ordering, title, mDefaultShowAsAction);
    391 
    392         if (mCurrentMenuInfo != null) {
    393             // Pass along the current menu info
    394             item.setMenuInfo(mCurrentMenuInfo);
    395         }
    396 
    397         mItems.add(findInsertIndex(mItems, ordering), item);
    398         onItemsChanged(true);
    399 
    400         return item;
    401     }
    402 
    403     public MenuItem add(CharSequence title) {
    404         return addInternal(0, 0, 0, title);
    405     }
    406 
    407     public MenuItem add(int titleRes) {
    408         return addInternal(0, 0, 0, mResources.getString(titleRes));
    409     }
    410 
    411     public MenuItem add(int group, int id, int categoryOrder, CharSequence title) {
    412         return addInternal(group, id, categoryOrder, title);
    413     }
    414 
    415     public MenuItem add(int group, int id, int categoryOrder, int title) {
    416         return addInternal(group, id, categoryOrder, mResources.getString(title));
    417     }
    418 
    419     public SubMenu addSubMenu(CharSequence title) {
    420         return addSubMenu(0, 0, 0, title);
    421     }
    422 
    423     public SubMenu addSubMenu(int titleRes) {
    424         return addSubMenu(0, 0, 0, mResources.getString(titleRes));
    425     }
    426 
    427     public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
    428         final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title);
    429         final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item);
    430         item.setSubMenu(subMenu);
    431 
    432         return subMenu;
    433     }
    434 
    435     public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) {
    436         return addSubMenu(group, id, categoryOrder, mResources.getString(title));
    437     }
    438 
    439     public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller,
    440             Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) {
    441         PackageManager pm = mContext.getPackageManager();
    442         final List<ResolveInfo> lri =
    443                 pm.queryIntentActivityOptions(caller, specifics, intent, 0);
    444         final int N = lri != null ? lri.size() : 0;
    445 
    446         if ((flags & FLAG_APPEND_TO_GROUP) == 0) {
    447             removeGroup(group);
    448         }
    449 
    450         for (int i=0; i<N; i++) {
    451             final ResolveInfo ri = lri.get(i);
    452             Intent rintent = new Intent(
    453                 ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]);
    454             rintent.setComponent(new ComponentName(
    455                     ri.activityInfo.applicationInfo.packageName,
    456                     ri.activityInfo.name));
    457             final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm))
    458                     .setIcon(ri.loadIcon(pm))
    459                     .setIntent(rintent);
    460             if (outSpecificItems != null && ri.specificIndex >= 0) {
    461                 outSpecificItems[ri.specificIndex] = item;
    462             }
    463         }
    464 
    465         return N;
    466     }
    467 
    468     public void removeItem(int id) {
    469         removeItemAtInt(findItemIndex(id), true);
    470     }
    471 
    472     public void removeGroup(int group) {
    473         final int i = findGroupIndex(group);
    474 
    475         if (i >= 0) {
    476             final int maxRemovable = mItems.size() - i;
    477             int numRemoved = 0;
    478             while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) {
    479                 // Don't force update for each one, this method will do it at the end
    480                 removeItemAtInt(i, false);
    481             }
    482 
    483             // Notify menu views
    484             onItemsChanged(true);
    485         }
    486     }
    487 
    488     /**
    489      * Remove the item at the given index and optionally forces menu views to
    490      * update.
    491      *
    492      * @param index The index of the item to be removed. If this index is
    493      *            invalid an exception is thrown.
    494      * @param updateChildrenOnMenuViews Whether to force update on menu views.
    495      *            Please make sure you eventually call this after your batch of
    496      *            removals.
    497      */
    498     private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) {
    499         if ((index < 0) || (index >= mItems.size())) return;
    500 
    501         mItems.remove(index);
    502 
    503         if (updateChildrenOnMenuViews) onItemsChanged(true);
    504     }
    505 
    506     public void removeItemAt(int index) {
    507         removeItemAtInt(index, true);
    508     }
    509 
    510     public void clearAll() {
    511         mPreventDispatchingItemsChanged = true;
    512         clear();
    513         clearHeader();
    514         mPreventDispatchingItemsChanged = false;
    515         mItemsChangedWhileDispatchPrevented = false;
    516         onItemsChanged(true);
    517     }
    518 
    519     public void clear() {
    520         if (mExpandedItem != null) {
    521             collapseItemActionView(mExpandedItem);
    522         }
    523         mItems.clear();
    524 
    525         onItemsChanged(true);
    526     }
    527 
    528     void setExclusiveItemChecked(MenuItem item) {
    529         final int group = item.getGroupId();
    530 
    531         final int N = mItems.size();
    532         for (int i = 0; i < N; i++) {
    533             MenuItemImpl curItem = mItems.get(i);
    534             if (curItem.getGroupId() == group) {
    535                 if (!curItem.isExclusiveCheckable()) continue;
    536                 if (!curItem.isCheckable()) continue;
    537 
    538                 // Check the item meant to be checked, uncheck the others (that are in the group)
    539                 curItem.setCheckedInt(curItem == item);
    540             }
    541         }
    542     }
    543 
    544     public void setGroupCheckable(int group, boolean checkable, boolean exclusive) {
    545         final int N = mItems.size();
    546 
    547         for (int i = 0; i < N; i++) {
    548             MenuItemImpl item = mItems.get(i);
    549             if (item.getGroupId() == group) {
    550                 item.setExclusiveCheckable(exclusive);
    551                 item.setCheckable(checkable);
    552             }
    553         }
    554     }
    555 
    556     public void setGroupVisible(int group, boolean visible) {
    557         final int N = mItems.size();
    558 
    559         // We handle the notification of items being changed ourselves, so we use setVisibleInt rather
    560         // than setVisible and at the end notify of items being changed
    561 
    562         boolean changedAtLeastOneItem = false;
    563         for (int i = 0; i < N; i++) {
    564             MenuItemImpl item = mItems.get(i);
    565             if (item.getGroupId() == group) {
    566                 if (item.setVisibleInt(visible)) changedAtLeastOneItem = true;
    567             }
    568         }
    569 
    570         if (changedAtLeastOneItem) onItemsChanged(true);
    571     }
    572 
    573     public void setGroupEnabled(int group, boolean enabled) {
    574         final int N = mItems.size();
    575 
    576         for (int i = 0; i < N; i++) {
    577             MenuItemImpl item = mItems.get(i);
    578             if (item.getGroupId() == group) {
    579                 item.setEnabled(enabled);
    580             }
    581         }
    582     }
    583 
    584     public boolean hasVisibleItems() {
    585         final int size = size();
    586 
    587         for (int i = 0; i < size; i++) {
    588             MenuItemImpl item = mItems.get(i);
    589             if (item.isVisible()) {
    590                 return true;
    591             }
    592         }
    593 
    594         return false;
    595     }
    596 
    597     public MenuItem findItem(int id) {
    598         final int size = size();
    599         for (int i = 0; i < size; i++) {
    600             MenuItemImpl item = mItems.get(i);
    601             if (item.getItemId() == id) {
    602                 return item;
    603             } else if (item.hasSubMenu()) {
    604                 MenuItem possibleItem = item.getSubMenu().findItem(id);
    605 
    606                 if (possibleItem != null) {
    607                     return possibleItem;
    608                 }
    609             }
    610         }
    611 
    612         return null;
    613     }
    614 
    615     public int findItemIndex(int id) {
    616         final int size = size();
    617 
    618         for (int i = 0; i < size; i++) {
    619             MenuItemImpl item = mItems.get(i);
    620             if (item.getItemId() == id) {
    621                 return i;
    622             }
    623         }
    624 
    625         return -1;
    626     }
    627 
    628     public int findGroupIndex(int group) {
    629         return findGroupIndex(group, 0);
    630     }
    631 
    632     public int findGroupIndex(int group, int start) {
    633         final int size = size();
    634 
    635         if (start < 0) {
    636             start = 0;
    637         }
    638 
    639         for (int i = start; i < size; i++) {
    640             final MenuItemImpl item = mItems.get(i);
    641 
    642             if (item.getGroupId() == group) {
    643                 return i;
    644             }
    645         }
    646 
    647         return -1;
    648     }
    649 
    650     public int size() {
    651         return mItems.size();
    652     }
    653 
    654     /** {@inheritDoc} */
    655     public MenuItem getItem(int index) {
    656         return mItems.get(index);
    657     }
    658 
    659     public boolean isShortcutKey(int keyCode, KeyEvent event) {
    660         return findItemWithShortcutForKey(keyCode, event) != null;
    661     }
    662 
    663     public void setQwertyMode(boolean isQwerty) {
    664         mQwertyMode = isQwerty;
    665 
    666         onItemsChanged(false);
    667     }
    668 
    669     /**
    670      * Returns the ordering across all items. This will grab the category from
    671      * the upper bits, find out how to order the category with respect to other
    672      * categories, and combine it with the lower bits.
    673      *
    674      * @param categoryOrder The category order for a particular item (if it has
    675      *            not been or/add with a category, the default category is
    676      *            assumed).
    677      * @return An ordering integer that can be used to order this item across
    678      *         all the items (even from other categories).
    679      */
    680     private static int getOrdering(int categoryOrder) {
    681         final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT;
    682 
    683         if (index < 0 || index >= sCategoryToOrder.length) {
    684             throw new IllegalArgumentException("order does not contain a valid category.");
    685         }
    686 
    687         return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK);
    688     }
    689 
    690     /**
    691      * @return whether the menu shortcuts are in qwerty mode or not
    692      */
    693     boolean isQwertyMode() {
    694         return mQwertyMode;
    695     }
    696 
    697     /**
    698      * Sets whether the shortcuts should be visible on menus.  Devices without hardware
    699      * key input will never make shortcuts visible even if this method is passed 'true'.
    700      *
    701      * @param shortcutsVisible Whether shortcuts should be visible (if true and a
    702      *            menu item does not have a shortcut defined, that item will
    703      *            still NOT show a shortcut)
    704      */
    705     public void setShortcutsVisible(boolean shortcutsVisible) {
    706         if (mShortcutsVisible == shortcutsVisible) return;
    707 
    708         setShortcutsVisibleInner(shortcutsVisible);
    709         onItemsChanged(false);
    710     }
    711 
    712     private void setShortcutsVisibleInner(boolean shortcutsVisible) {
    713         mShortcutsVisible = shortcutsVisible
    714                 && mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS
    715                 && mResources.getBoolean(
    716                         com.android.internal.R.bool.config_showMenuShortcutsWhenKeyboardPresent);
    717     }
    718 
    719     /**
    720      * @return Whether shortcuts should be visible on menus.
    721      */
    722     public boolean isShortcutsVisible() {
    723         return mShortcutsVisible;
    724     }
    725 
    726     Resources getResources() {
    727         return mResources;
    728     }
    729 
    730     public Context getContext() {
    731         return mContext;
    732     }
    733 
    734     boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) {
    735         return mCallback != null && mCallback.onMenuItemSelected(menu, item);
    736     }
    737 
    738     /**
    739      * Dispatch a mode change event to this menu's callback.
    740      */
    741     public void changeMenuMode() {
    742         if (mCallback != null) {
    743             mCallback.onMenuModeChange(this);
    744         }
    745     }
    746 
    747     private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) {
    748         for (int i = items.size() - 1; i >= 0; i--) {
    749             MenuItemImpl item = items.get(i);
    750             if (item.getOrdering() <= ordering) {
    751                 return i + 1;
    752             }
    753         }
    754 
    755         return 0;
    756     }
    757 
    758     public boolean performShortcut(int keyCode, KeyEvent event, int flags) {
    759         final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event);
    760 
    761         boolean handled = false;
    762 
    763         if (item != null) {
    764             handled = performItemAction(item, flags);
    765         }
    766 
    767         if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) {
    768             close(true);
    769         }
    770 
    771         return handled;
    772     }
    773 
    774     /*
    775      * This function will return all the menu and sub-menu items that can
    776      * be directly (the shortcut directly corresponds) and indirectly
    777      * (the ALT-enabled char corresponds to the shortcut) associated
    778      * with the keyCode.
    779      */
    780     void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) {
    781         final boolean qwerty = isQwertyMode();
    782         final int metaState = event.getMetaState();
    783         final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
    784         // Get the chars associated with the keyCode (i.e using any chording combo)
    785         final boolean isKeyCodeMapped = event.getKeyData(possibleChars);
    786         // The delete key is not mapped to '\b' so we treat it specially
    787         if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) {
    788             return;
    789         }
    790 
    791         // Look for an item whose shortcut is this key.
    792         final int N = mItems.size();
    793         for (int i = 0; i < N; i++) {
    794             MenuItemImpl item = mItems.get(i);
    795             if (item.hasSubMenu()) {
    796                 ((MenuBuilder)item.getSubMenu()).findItemsWithShortcutForKey(items, keyCode, event);
    797             }
    798             final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut();
    799             if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) &&
    800                   (shortcutChar != 0) &&
    801                   (shortcutChar == possibleChars.meta[0]
    802                       || shortcutChar == possibleChars.meta[2]
    803                       || (qwerty && shortcutChar == '\b' &&
    804                           keyCode == KeyEvent.KEYCODE_DEL)) &&
    805                   item.isEnabled()) {
    806                 items.add(item);
    807             }
    808         }
    809     }
    810 
    811     /*
    812      * We want to return the menu item associated with the key, but if there is no
    813      * ambiguity (i.e. there is only one menu item corresponding to the key) we want
    814      * to return it even if it's not an exact match; this allow the user to
    815      * _not_ use the ALT key for example, making the use of shortcuts slightly more
    816      * user-friendly. An example is on the G1, '!' and '1' are on the same key, and
    817      * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut).
    818      *
    819      * On the other hand, if two (or more) shortcuts corresponds to the same key,
    820      * we have to only return the exact match.
    821      */
    822     MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) {
    823         // Get all items that can be associated directly or indirectly with the keyCode
    824         ArrayList<MenuItemImpl> items = mTempShortcutItemList;
    825         items.clear();
    826         findItemsWithShortcutForKey(items, keyCode, event);
    827 
    828         if (items.isEmpty()) {
    829             return null;
    830         }
    831 
    832         final int metaState = event.getMetaState();
    833         final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
    834         // Get the chars associated with the keyCode (i.e using any chording combo)
    835         event.getKeyData(possibleChars);
    836 
    837         // If we have only one element, we can safely returns it
    838         final int size = items.size();
    839         if (size == 1) {
    840             return items.get(0);
    841         }
    842 
    843         final boolean qwerty = isQwertyMode();
    844         // If we found more than one item associated with the key,
    845         // we have to return the exact match
    846         for (int i = 0; i < size; i++) {
    847             final MenuItemImpl item = items.get(i);
    848             final char shortcutChar = qwerty ? item.getAlphabeticShortcut() :
    849                     item.getNumericShortcut();
    850             if ((shortcutChar == possibleChars.meta[0] &&
    851                     (metaState & KeyEvent.META_ALT_ON) == 0)
    852                 || (shortcutChar == possibleChars.meta[2] &&
    853                     (metaState & KeyEvent.META_ALT_ON) != 0)
    854                 || (qwerty && shortcutChar == '\b' &&
    855                     keyCode == KeyEvent.KEYCODE_DEL)) {
    856                 return item;
    857             }
    858         }
    859         return null;
    860     }
    861 
    862     public boolean performIdentifierAction(int id, int flags) {
    863         // Look for an item whose identifier is the id.
    864         return performItemAction(findItem(id), flags);
    865     }
    866 
    867     public boolean performItemAction(MenuItem item, int flags) {
    868         MenuItemImpl itemImpl = (MenuItemImpl) item;
    869 
    870         if (itemImpl == null || !itemImpl.isEnabled()) {
    871             return false;
    872         }
    873 
    874         boolean invoked = itemImpl.invoke();
    875 
    876         final ActionProvider provider = item.getActionProvider();
    877         final boolean providerHasSubMenu = provider != null && provider.hasSubMenu();
    878         if (itemImpl.hasCollapsibleActionView()) {
    879             invoked |= itemImpl.expandActionView();
    880             if (invoked) close(true);
    881         } else if (itemImpl.hasSubMenu() || providerHasSubMenu) {
    882             close(false);
    883 
    884             if (!itemImpl.hasSubMenu()) {
    885                 itemImpl.setSubMenu(new SubMenuBuilder(getContext(), this, itemImpl));
    886             }
    887 
    888             final SubMenuBuilder subMenu = (SubMenuBuilder) itemImpl.getSubMenu();
    889             if (providerHasSubMenu) {
    890                 provider.onPrepareSubMenu(subMenu);
    891             }
    892             invoked |= dispatchSubMenuSelected(subMenu);
    893             if (!invoked) close(true);
    894         } else {
    895             if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) {
    896                 close(true);
    897             }
    898         }
    899 
    900         return invoked;
    901     }
    902 
    903     /**
    904      * Closes the visible menu.
    905      *
    906      * @param allMenusAreClosing Whether the menus are completely closing (true),
    907      *            or whether there is another menu coming in this menu's place
    908      *            (false). For example, if the menu is closing because a
    909      *            sub menu is about to be shown, <var>allMenusAreClosing</var>
    910      *            is false.
    911      */
    912     final void close(boolean allMenusAreClosing) {
    913         if (mIsClosing) return;
    914 
    915         mIsClosing = true;
    916         for (WeakReference<MenuPresenter> ref : mPresenters) {
    917             final MenuPresenter presenter = ref.get();
    918             if (presenter == null) {
    919                 mPresenters.remove(ref);
    920             } else {
    921                 presenter.onCloseMenu(this, allMenusAreClosing);
    922             }
    923         }
    924         mIsClosing = false;
    925     }
    926 
    927     /** {@inheritDoc} */
    928     public void close() {
    929         close(true);
    930     }
    931 
    932     /**
    933      * Called when an item is added or removed.
    934      *
    935      * @param structureChanged true if the menu structure changed,
    936      *                         false if only item properties changed.
    937      *                         (Visibility is a structural property since it affects layout.)
    938      */
    939     void onItemsChanged(boolean structureChanged) {
    940         if (!mPreventDispatchingItemsChanged) {
    941             if (structureChanged) {
    942                 mIsVisibleItemsStale = true;
    943                 mIsActionItemsStale = true;
    944             }
    945 
    946             dispatchPresenterUpdate(structureChanged);
    947         } else {
    948             mItemsChangedWhileDispatchPrevented = true;
    949         }
    950     }
    951 
    952     /**
    953      * Stop dispatching item changed events to presenters until
    954      * {@link #startDispatchingItemsChanged()} is called. Useful when
    955      * many menu operations are going to be performed as a batch.
    956      */
    957     public void stopDispatchingItemsChanged() {
    958         if (!mPreventDispatchingItemsChanged) {
    959             mPreventDispatchingItemsChanged = true;
    960             mItemsChangedWhileDispatchPrevented = false;
    961         }
    962     }
    963 
    964     public void startDispatchingItemsChanged() {
    965         mPreventDispatchingItemsChanged = false;
    966 
    967         if (mItemsChangedWhileDispatchPrevented) {
    968             mItemsChangedWhileDispatchPrevented = false;
    969             onItemsChanged(true);
    970         }
    971     }
    972 
    973     /**
    974      * Called by {@link MenuItemImpl} when its visible flag is changed.
    975      * @param item The item that has gone through a visibility change.
    976      */
    977     void onItemVisibleChanged(MenuItemImpl item) {
    978         // Notify of items being changed
    979         mIsVisibleItemsStale = true;
    980         onItemsChanged(true);
    981     }
    982 
    983     /**
    984      * Called by {@link MenuItemImpl} when its action request status is changed.
    985      * @param item The item that has gone through a change in action request status.
    986      */
    987     void onItemActionRequestChanged(MenuItemImpl item) {
    988         // Notify of items being changed
    989         mIsActionItemsStale = true;
    990         onItemsChanged(true);
    991     }
    992 
    993     ArrayList<MenuItemImpl> getVisibleItems() {
    994         if (!mIsVisibleItemsStale) return mVisibleItems;
    995 
    996         // Refresh the visible items
    997         mVisibleItems.clear();
    998 
    999         final int itemsSize = mItems.size();
   1000         MenuItemImpl item;
   1001         for (int i = 0; i < itemsSize; i++) {
   1002             item = mItems.get(i);
   1003             if (item.isVisible()) mVisibleItems.add(item);
   1004         }
   1005 
   1006         mIsVisibleItemsStale = false;
   1007         mIsActionItemsStale = true;
   1008 
   1009         return mVisibleItems;
   1010     }
   1011 
   1012     /**
   1013      * This method determines which menu items get to be 'action items' that will appear
   1014      * in an action bar and which items should be 'overflow items' in a secondary menu.
   1015      * The rules are as follows:
   1016      *
   1017      * <p>Items are considered for inclusion in the order specified within the menu.
   1018      * There is a limit of mMaxActionItems as a total count, optionally including the overflow
   1019      * menu button itself. This is a soft limit; if an item shares a group ID with an item
   1020      * previously included as an action item, the new item will stay with its group and become
   1021      * an action item itself even if it breaks the max item count limit. This is done to
   1022      * limit the conceptual complexity of the items presented within an action bar. Only a few
   1023      * unrelated concepts should be presented to the user in this space, and groups are treated
   1024      * as a single concept.
   1025      *
   1026      * <p>There is also a hard limit of consumed measurable space: mActionWidthLimit. This
   1027      * limit may be broken by a single item that exceeds the remaining space, but no further
   1028      * items may be added. If an item that is part of a group cannot fit within the remaining
   1029      * measured width, the entire group will be demoted to overflow. This is done to ensure room
   1030      * for navigation and other affordances in the action bar as well as reduce general UI clutter.
   1031      *
   1032      * <p>The space freed by demoting a full group cannot be consumed by future menu items.
   1033      * Once items begin to overflow, all future items become overflow items as well. This is
   1034      * to avoid inadvertent reordering that may break the app's intended design.
   1035      */
   1036     public void flagActionItems() {
   1037         if (!mIsActionItemsStale) {
   1038             return;
   1039         }
   1040 
   1041         // Presenters flag action items as needed.
   1042         boolean flagged = false;
   1043         for (WeakReference<MenuPresenter> ref : mPresenters) {
   1044             final MenuPresenter presenter = ref.get();
   1045             if (presenter == null) {
   1046                 mPresenters.remove(ref);
   1047             } else {
   1048                 flagged |= presenter.flagActionItems();
   1049             }
   1050         }
   1051 
   1052         if (flagged) {
   1053             mActionItems.clear();
   1054             mNonActionItems.clear();
   1055             ArrayList<MenuItemImpl> visibleItems = getVisibleItems();
   1056             final int itemsSize = visibleItems.size();
   1057             for (int i = 0; i < itemsSize; i++) {
   1058                 MenuItemImpl item = visibleItems.get(i);
   1059                 if (item.isActionButton()) {
   1060                     mActionItems.add(item);
   1061                 } else {
   1062                     mNonActionItems.add(item);
   1063                 }
   1064             }
   1065         } else {
   1066             // Nobody flagged anything, everything is a non-action item.
   1067             // (This happens during a first pass with no action-item presenters.)
   1068             mActionItems.clear();
   1069             mNonActionItems.clear();
   1070             mNonActionItems.addAll(getVisibleItems());
   1071         }
   1072         mIsActionItemsStale = false;
   1073     }
   1074 
   1075     ArrayList<MenuItemImpl> getActionItems() {
   1076         flagActionItems();
   1077         return mActionItems;
   1078     }
   1079 
   1080     ArrayList<MenuItemImpl> getNonActionItems() {
   1081         flagActionItems();
   1082         return mNonActionItems;
   1083     }
   1084 
   1085     public void clearHeader() {
   1086         mHeaderIcon = null;
   1087         mHeaderTitle = null;
   1088         mHeaderView = null;
   1089 
   1090         onItemsChanged(false);
   1091     }
   1092 
   1093     private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes,
   1094             final Drawable icon, final View view) {
   1095         final Resources r = getResources();
   1096 
   1097         if (view != null) {
   1098             mHeaderView = view;
   1099 
   1100             // If using a custom view, then the title and icon aren't used
   1101             mHeaderTitle = null;
   1102             mHeaderIcon = null;
   1103         } else {
   1104             if (titleRes > 0) {
   1105                 mHeaderTitle = r.getText(titleRes);
   1106             } else if (title != null) {
   1107                 mHeaderTitle = title;
   1108             }
   1109 
   1110             if (iconRes > 0) {
   1111                 mHeaderIcon = r.getDrawable(iconRes);
   1112             } else if (icon != null) {
   1113                 mHeaderIcon = icon;
   1114             }
   1115 
   1116             // If using the title or icon, then a custom view isn't used
   1117             mHeaderView = null;
   1118         }
   1119 
   1120         // Notify of change
   1121         onItemsChanged(false);
   1122     }
   1123 
   1124     /**
   1125      * Sets the header's title. This replaces the header view. Called by the
   1126      * builder-style methods of subclasses.
   1127      *
   1128      * @param title The new title.
   1129      * @return This MenuBuilder so additional setters can be called.
   1130      */
   1131     protected MenuBuilder setHeaderTitleInt(CharSequence title) {
   1132         setHeaderInternal(0, title, 0, null, null);
   1133         return this;
   1134     }
   1135 
   1136     /**
   1137      * Sets the header's title. This replaces the header view. Called by the
   1138      * builder-style methods of subclasses.
   1139      *
   1140      * @param titleRes The new title (as a resource ID).
   1141      * @return This MenuBuilder so additional setters can be called.
   1142      */
   1143     protected MenuBuilder setHeaderTitleInt(int titleRes) {
   1144         setHeaderInternal(titleRes, null, 0, null, null);
   1145         return this;
   1146     }
   1147 
   1148     /**
   1149      * Sets the header's icon. This replaces the header view. Called by the
   1150      * builder-style methods of subclasses.
   1151      *
   1152      * @param icon The new icon.
   1153      * @return This MenuBuilder so additional setters can be called.
   1154      */
   1155     protected MenuBuilder setHeaderIconInt(Drawable icon) {
   1156         setHeaderInternal(0, null, 0, icon, null);
   1157         return this;
   1158     }
   1159 
   1160     /**
   1161      * Sets the header's icon. This replaces the header view. Called by the
   1162      * builder-style methods of subclasses.
   1163      *
   1164      * @param iconRes The new icon (as a resource ID).
   1165      * @return This MenuBuilder so additional setters can be called.
   1166      */
   1167     protected MenuBuilder setHeaderIconInt(int iconRes) {
   1168         setHeaderInternal(0, null, iconRes, null, null);
   1169         return this;
   1170     }
   1171 
   1172     /**
   1173      * Sets the header's view. This replaces the title and icon. Called by the
   1174      * builder-style methods of subclasses.
   1175      *
   1176      * @param view The new view.
   1177      * @return This MenuBuilder so additional setters can be called.
   1178      */
   1179     protected MenuBuilder setHeaderViewInt(View view) {
   1180         setHeaderInternal(0, null, 0, null, view);
   1181         return this;
   1182     }
   1183 
   1184     public CharSequence getHeaderTitle() {
   1185         return mHeaderTitle;
   1186     }
   1187 
   1188     public Drawable getHeaderIcon() {
   1189         return mHeaderIcon;
   1190     }
   1191 
   1192     public View getHeaderView() {
   1193         return mHeaderView;
   1194     }
   1195 
   1196     /**
   1197      * Gets the root menu (if this is a submenu, find its root menu).
   1198      * @return The root menu.
   1199      */
   1200     public MenuBuilder getRootMenu() {
   1201         return this;
   1202     }
   1203 
   1204     /**
   1205      * Sets the current menu info that is set on all items added to this menu
   1206      * (until this is called again with different menu info, in which case that
   1207      * one will be added to all subsequent item additions).
   1208      *
   1209      * @param menuInfo The extra menu information to add.
   1210      */
   1211     public void setCurrentMenuInfo(ContextMenuInfo menuInfo) {
   1212         mCurrentMenuInfo = menuInfo;
   1213     }
   1214 
   1215     void setOptionalIconsVisible(boolean visible) {
   1216         mOptionalIconsVisible = visible;
   1217     }
   1218 
   1219     boolean getOptionalIconsVisible() {
   1220         return mOptionalIconsVisible;
   1221     }
   1222 
   1223     public boolean expandItemActionView(MenuItemImpl item) {
   1224         if (mPresenters.isEmpty()) return false;
   1225 
   1226         boolean expanded = false;
   1227 
   1228         stopDispatchingItemsChanged();
   1229         for (WeakReference<MenuPresenter> ref : mPresenters) {
   1230             final MenuPresenter presenter = ref.get();
   1231             if (presenter == null) {
   1232                 mPresenters.remove(ref);
   1233             } else if ((expanded = presenter.expandItemActionView(this, item))) {
   1234                 break;
   1235             }
   1236         }
   1237         startDispatchingItemsChanged();
   1238 
   1239         if (expanded) {
   1240             mExpandedItem = item;
   1241         }
   1242         return expanded;
   1243     }
   1244 
   1245     public boolean collapseItemActionView(MenuItemImpl item) {
   1246         if (mPresenters.isEmpty() || mExpandedItem != item) return false;
   1247 
   1248         boolean collapsed = false;
   1249 
   1250         stopDispatchingItemsChanged();
   1251         for (WeakReference<MenuPresenter> ref : mPresenters) {
   1252             final MenuPresenter presenter = ref.get();
   1253             if (presenter == null) {
   1254                 mPresenters.remove(ref);
   1255             } else if ((collapsed = presenter.collapseItemActionView(this, item))) {
   1256                 break;
   1257             }
   1258         }
   1259         startDispatchingItemsChanged();
   1260 
   1261         if (collapsed) {
   1262             mExpandedItem = null;
   1263         }
   1264         return collapsed;
   1265     }
   1266 
   1267     public MenuItemImpl getExpandedItem() {
   1268         return mExpandedItem;
   1269     }
   1270 }
   1271