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