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