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