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