OSDN Git Service

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