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