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