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