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