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