2 * Copyright (C) 2006 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com
.actionbarsherlock
.internal
.view
.menu
;
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
;
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
;
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
;
49 * Implementation of the {@link android.view.Menu} interface for creating a
52 public class MenuBuilder
implements Menu
{
53 //UNUSED private static final String TAG = "MenuBuilder";
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";
59 private static final int[] sCategoryToOrder
= new int[] {
65 0, /* SELECTED_ALTERNATIVE */
68 private final Context mContext
;
69 private final Resources mResources
;
72 * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode()
73 * instead of accessing this directly.
75 private boolean mQwertyMode
;
78 * Whether the shortcuts should be visible on menus. Use isShortcutsVisible()
79 * instead of accessing this directly.
81 private boolean mShortcutsVisible
;
84 * Callback that will receive the various menu-related events generated by
85 * this class. Use getCallback to get a reference to the callback.
87 private Callback mCallback
;
89 /** Contains all of the items for this menu */
90 private ArrayList
<MenuItemImpl
> mItems
;
92 /** Contains only the items that are currently visible. This will be created/refreshed from
93 * {@link #getVisibleItems()} */
94 private ArrayList
<MenuItemImpl
> mVisibleItems
;
96 * Whether or not the items (or any one item's shown state) has changed since it was last
97 * fetched from {@link #getVisibleItems()}
99 private boolean mIsVisibleItemsStale
;
102 * Contains only the items that should appear in the Action Bar, if present.
104 private ArrayList
<MenuItemImpl
> mActionItems
;
106 * Contains items that should NOT appear in the Action Bar, if present.
108 private ArrayList
<MenuItemImpl
> mNonActionItems
;
111 * Whether or not the items (or any one item's action state) has changed since it was
114 private boolean mIsActionItemsStale
;
117 * Default value for how added items should show in the action list.
119 private int mDefaultShowAsAction
= MenuItem
.SHOW_AS_ACTION_NEVER
;
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.
126 private ContextMenuInfo mCurrentMenuInfo
;
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) */
136 * Contains the state of the View hierarchy for all menu views when the menu
139 //UNUSED private SparseArray<Parcelable> mFrozenViewStates;
142 * Prevents onItemsChanged from doing its junk, useful for batching commands
143 * that may individually call onItemsChanged.
145 private boolean mPreventDispatchingItemsChanged
= false
;
146 private boolean mItemsChangedWhileDispatchPrevented
= false
;
148 private boolean mOptionalIconsVisible
= false
;
150 private boolean mIsClosing
= false
;
152 private ArrayList
<MenuItemImpl
> mTempShortcutItemList
= new ArrayList
<MenuItemImpl
>();
154 private CopyOnWriteArrayList
<WeakReference
<MenuPresenter
>> mPresenters
=
155 new CopyOnWriteArrayList
<WeakReference
<MenuPresenter
>>();
158 * Currently expanded menu item; must be collapsed when we clear.
160 private MenuItemImpl mExpandedItem
;
163 * Called by menu to notify of close and selection changes.
165 public interface Callback
{
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
172 public boolean onMenuItemSelected(MenuBuilder menu
, MenuItem item
);
175 * Called when the mode of the menu changes (for example, from icon to expanded).
177 * @param menu the menu that has changed modes
179 public void onMenuModeChange(MenuBuilder menu
);
183 * Called by menu items to execute their associated action
185 public interface ItemInvoker
{
186 public boolean invokeItem(MenuItemImpl item
);
189 public MenuBuilder(Context context
) {
191 mResources
= context
.getResources();
193 mItems
= new ArrayList
<MenuItemImpl
>();
195 mVisibleItems
= new ArrayList
<MenuItemImpl
>();
196 mIsVisibleItemsStale
= true
;
198 mActionItems
= new ArrayList
<MenuItemImpl
>();
199 mNonActionItems
= new ArrayList
<MenuItemImpl
>();
200 mIsActionItemsStale
= true
;
202 setShortcutsVisibleInner(true
);
205 public MenuBuilder
setDefaultShowAsAction(int defaultShowAsAction
) {
206 mDefaultShowAsAction
= defaultShowAsAction
;
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)}.
215 * @param presenter The presenter to add
217 public void addMenuPresenter(MenuPresenter presenter
) {
218 mPresenters
.add(new WeakReference
<MenuPresenter
>(presenter
));
219 presenter
.initForMenu(mContext
, this);
220 mIsActionItemsStale
= true
;
224 * Remove a presenter from this menu. That presenter will no longer
225 * receive notifications of updates to this menu's data.
227 * @param presenter The presenter to remove
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
);
238 private void dispatchPresenterUpdate(boolean cleared
) {
239 if (mPresenters
.isEmpty()) return;
241 stopDispatchingItemsChanged();
242 for (WeakReference
<MenuPresenter
> ref
: mPresenters
) {
243 final MenuPresenter presenter
= ref
.get();
244 if (presenter
== null
) {
245 mPresenters
.remove(ref
);
247 presenter
.updateMenuView(cleared
);
250 startDispatchingItemsChanged();
253 private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu
) {
254 if (mPresenters
.isEmpty()) return false
;
256 boolean result
= false
;
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
);
269 private void dispatchSaveInstanceState(Bundle outState
) {
270 if (mPresenters
.isEmpty()) return;
272 SparseArray
<Parcelable
> presenterStates
= new SparseArray
<Parcelable
>();
274 for (WeakReference
<MenuPresenter
> ref
: mPresenters
) {
275 final MenuPresenter presenter
= ref
.get();
276 if (presenter
== null
) {
277 mPresenters
.remove(ref
);
279 final int id
= presenter
.getId();
281 final Parcelable state
= presenter
.onSaveInstanceState();
283 presenterStates
.put(id
, state
);
289 outState
.putSparseParcelableArray(PRESENTER_KEY
, presenterStates
);
292 private void dispatchRestoreInstanceState(Bundle state
) {
293 SparseArray
<Parcelable
> presenterStates
= state
.getSparseParcelableArray(PRESENTER_KEY
);
295 if (presenterStates
== null
|| mPresenters
.isEmpty()) return;
297 for (WeakReference
<MenuPresenter
> ref
: mPresenters
) {
298 final MenuPresenter presenter
= ref
.get();
299 if (presenter
== null
) {
300 mPresenters
.remove(ref
);
302 final int id
= presenter
.getId();
304 Parcelable parcel
= presenterStates
.get(id
);
305 if (parcel
!= null
) {
306 presenter
.onRestoreInstanceState(parcel
);
313 public void savePresenterStates(Bundle outState
) {
314 dispatchSaveInstanceState(outState
);
317 public void restorePresenterStates(Bundle state
) {
318 dispatchRestoreInstanceState(state
);
321 public void saveActionViewStates(Bundle outStates
) {
322 SparseArray
<Parcelable
> viewStates
= null
;
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
>();
332 v
.saveHierarchyState(viewStates
);
333 if (item
.isActionViewExpanded()) {
334 outStates
.putInt(EXPANDED_ACTION_VIEW_ID
, item
.getItemId());
337 if (item
.hasSubMenu()) {
338 final SubMenuBuilder subMenu
= (SubMenuBuilder
) item
.getSubMenu();
339 subMenu
.saveActionViewStates(outStates
);
343 if (viewStates
!= null
) {
344 outStates
.putSparseParcelableArray(getActionViewStatesKey(), viewStates
);
348 public void restoreActionViewStates(Bundle states
) {
349 if (states
== null
) {
353 SparseArray
<Parcelable
> viewStates
= states
.getSparseParcelableArray(
354 getActionViewStatesKey());
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
);
363 if (item
.hasSubMenu()) {
364 final SubMenuBuilder subMenu
= (SubMenuBuilder
) item
.getSubMenu();
365 subMenu
.restoreActionViewStates(states
);
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();
378 protected String
getActionViewStatesKey() {
379 return ACTION_VIEW_STATES_KEY
;
382 public void setCallback(Callback cb
) {
387 * Adds an item to the menu. The other add methods funnel to this.
389 private MenuItem
addInternal(int group
, int id
, int categoryOrder
, CharSequence title
) {
390 final int ordering
= getOrdering(categoryOrder
);
392 final MenuItemImpl item
= new MenuItemImpl(this, group
, id
, categoryOrder
,
393 ordering
, title
, mDefaultShowAsAction
);
395 if (mCurrentMenuInfo
!= null
) {
396 // Pass along the current menu info
397 item
.setMenuInfo(mCurrentMenuInfo
);
400 mItems
.add(findInsertIndex(mItems
, ordering
), item
);
401 onItemsChanged(true
);
406 public MenuItem
add(CharSequence title
) {
407 return addInternal(0, 0, 0, title
);
410 public MenuItem
add(int titleRes
) {
411 return addInternal(0, 0, 0, mResources
.getString(titleRes
));
414 public MenuItem
add(int group
, int id
, int categoryOrder
, CharSequence title
) {
415 return addInternal(group
, id
, categoryOrder
, title
);
418 public MenuItem
add(int group
, int id
, int categoryOrder
, int title
) {
419 return addInternal(group
, id
, categoryOrder
, mResources
.getString(title
));
422 public SubMenu
addSubMenu(CharSequence title
) {
423 return addSubMenu(0, 0, 0, title
);
426 public SubMenu
addSubMenu(int titleRes
) {
427 return addSubMenu(0, 0, 0, mResources
.getString(titleRes
));
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
);
438 public SubMenu
addSubMenu(int group
, int id
, int categoryOrder
, int title
) {
439 return addSubMenu(group
, id
, categoryOrder
, mResources
.getString(title
));
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;
449 if ((flags
& FLAG_APPEND_TO_GROUP
) == 0) {
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
))
463 if (outSpecificItems
!= null
&& ri
.specificIndex
>= 0) {
464 outSpecificItems
[ri
.specificIndex
] = item
;
471 public void removeItem(int id
) {
472 removeItemAtInt(findItemIndex(id
), true
);
475 public void removeGroup(int group
) {
476 final int i
= findGroupIndex(group
);
479 final int maxRemovable
= mItems
.size() - i
;
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
);
487 onItemsChanged(true
);
492 * Remove the item at the given index and optionally forces menu views to
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
501 private void removeItemAtInt(int index
, boolean updateChildrenOnMenuViews
) {
502 if ((index
< 0) || (index
>= mItems
.size())) return;
504 mItems
.remove(index
);
506 if (updateChildrenOnMenuViews
) onItemsChanged(true
);
509 public void removeItemAt(int index
) {
510 removeItemAtInt(index
, true
);
513 public void clearAll() {
514 mPreventDispatchingItemsChanged
= true
;
517 mPreventDispatchingItemsChanged
= false
;
518 mItemsChangedWhileDispatchPrevented
= false
;
519 onItemsChanged(true
);
522 public void clear() {
523 if (mExpandedItem
!= null
) {
524 collapseItemActionView(mExpandedItem
);
528 onItemsChanged(true
);
531 void setExclusiveItemChecked(MenuItem item
) {
532 final int group
= item
.getGroupId();
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;
541 // Check the item meant to be checked, uncheck the others (that are in the group)
542 curItem
.setCheckedInt(curItem
== item
);
547 public void setGroupCheckable(int group
, boolean checkable
, boolean exclusive
) {
548 final int N
= mItems
.size();
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
);
559 public void setGroupVisible(int group
, boolean visible
) {
560 final int N
= mItems
.size();
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
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
;
573 if (changedAtLeastOneItem
) onItemsChanged(true
);
576 public void setGroupEnabled(int group
, boolean enabled
) {
577 final int N
= mItems
.size();
579 for (int i
= 0; i
< N
; i
++) {
580 MenuItemImpl item
= mItems
.get(i
);
581 if (item
.getGroupId() == group
) {
582 item
.setEnabled(enabled
);
587 public boolean hasVisibleItems() {
588 final int size
= size();
590 for (int i
= 0; i
< size
; i
++) {
591 MenuItemImpl item
= mItems
.get(i
);
592 if (item
.isVisible()) {
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
) {
606 } else if (item
.hasSubMenu()) {
607 MenuItem possibleItem
= item
.getSubMenu().findItem(id
);
609 if (possibleItem
!= null
) {
618 public int findItemIndex(int id
) {
619 final int size
= size();
621 for (int i
= 0; i
< size
; i
++) {
622 MenuItemImpl item
= mItems
.get(i
);
623 if (item
.getItemId() == id
) {
631 public int findGroupIndex(int group
) {
632 return findGroupIndex(group
, 0);
635 public int findGroupIndex(int group
, int start
) {
636 final int size
= size();
642 for (int i
= start
; i
< size
; i
++) {
643 final MenuItemImpl item
= mItems
.get(i
);
645 if (item
.getGroupId() == group
) {
654 return mItems
.size();
658 public MenuItem
getItem(int index
) {
659 return mItems
.get(index
);
662 public boolean isShortcutKey(int keyCode
, KeyEvent event
) {
663 return findItemWithShortcutForKey(keyCode
, event
) != null
;
666 public void setQwertyMode(boolean isQwerty
) {
667 mQwertyMode
= isQwerty
;
669 onItemsChanged(false
);
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.
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
680 * @return An ordering integer that can be used to order this item across
681 * all the items (even from other categories).
683 private static int getOrdering(int categoryOrder
) {
684 final int index
= (categoryOrder
& CATEGORY_MASK
) >> CATEGORY_SHIFT
;
686 if (index
< 0 || index
>= sCategoryToOrder
.length
) {
687 throw new IllegalArgumentException("order does not contain a valid category.");
690 return (sCategoryToOrder
[index
] << CATEGORY_SHIFT
) | (categoryOrder
& USER_MASK
);
694 * @return whether the menu shortcuts are in qwerty mode or not
696 boolean isQwertyMode() {
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'.
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)
708 public void setShortcutsVisible(boolean shortcutsVisible
) {
709 if (mShortcutsVisible
== shortcutsVisible
) return;
711 setShortcutsVisibleInner(shortcutsVisible
);
712 onItemsChanged(false
);
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
);
723 * @return Whether shortcuts should be visible on menus.
725 public boolean isShortcutsVisible() {
726 return mShortcutsVisible
;
729 Resources
getResources() {
733 public Context
getContext() {
737 boolean dispatchMenuItemSelected(MenuBuilder menu
, MenuItem item
) {
738 return mCallback
!= null
&& mCallback
.onMenuItemSelected(menu
, item
);
742 * Dispatch a mode change event to this menu's callback.
744 public void changeMenuMode() {
745 if (mCallback
!= null
) {
746 mCallback
.onMenuModeChange(this);
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
) {
761 public boolean performShortcut(int keyCode
, KeyEvent event
, int flags
) {
762 final MenuItemImpl item
= findItemWithShortcutForKey(keyCode
, event
);
764 boolean handled
= false
;
767 handled
= performItemAction(item
, flags
);
770 if ((flags
& FLAG_ALWAYS_PERFORM_CLOSE
) != 0) {
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
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
)) {
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
);
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
)) &&
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).
823 * On the other hand, if two (or more) shortcuts corresponds to the same key,
824 * we have to only return the exact match.
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
;
831 findItemsWithShortcutForKey(items
, keyCode
, event
);
833 if (items
.isEmpty()) {
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
);
842 // If we have only one element, we can safely returns it
843 final int size
= items
.size();
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
)) {
867 public boolean performIdentifierAction(int id
, int flags
) {
868 // Look for an item whose identifier is the id.
869 return performItemAction(findItem(id
), flags
);
872 public boolean performItemAction(MenuItem item
, int flags
) {
873 MenuItemImpl itemImpl
= (MenuItemImpl
) item
;
875 if (itemImpl
== null
|| !itemImpl
.isEnabled()) {
879 boolean invoked
= itemImpl
.invoke();
881 if (itemImpl
.hasCollapsibleActionView()) {
882 invoked
|= itemImpl
.expandActionView();
883 if (invoked
) close(true
);
884 } else if (item
.hasSubMenu()) {
887 final SubMenuBuilder subMenu
= (SubMenuBuilder
) item
.getSubMenu();
888 final ActionProvider provider
= item
.getActionProvider();
889 if (provider
!= null
&& provider
.hasSubMenu()) {
890 provider
.onPrepareSubMenu(subMenu
);
892 invoked
|= dispatchSubMenuSelected(subMenu
);
893 if (!invoked
) close(true
);
895 if ((flags
& FLAG_PERFORM_NO_CLOSE
) == 0) {
904 * Closes the visible menu.
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>
912 final void close(boolean allMenusAreClosing
) {
913 if (mIsClosing
) return;
916 for (WeakReference
<MenuPresenter
> ref
: mPresenters
) {
917 final MenuPresenter presenter
= ref
.get();
918 if (presenter
== null
) {
919 mPresenters
.remove(ref
);
921 presenter
.onCloseMenu(this, allMenusAreClosing
);
928 public void close() {
933 * Called when an item is added or removed.
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.)
939 void onItemsChanged(boolean structureChanged
) {
940 if (!mPreventDispatchingItemsChanged
) {
941 if (structureChanged
) {
942 mIsVisibleItemsStale
= true
;
943 mIsActionItemsStale
= true
;
946 dispatchPresenterUpdate(structureChanged
);
948 mItemsChangedWhileDispatchPrevented
= true
;
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.
957 public void stopDispatchingItemsChanged() {
958 if (!mPreventDispatchingItemsChanged
) {
959 mPreventDispatchingItemsChanged
= true
;
960 mItemsChangedWhileDispatchPrevented
= false
;
964 public void startDispatchingItemsChanged() {
965 mPreventDispatchingItemsChanged
= false
;
967 if (mItemsChangedWhileDispatchPrevented
) {
968 mItemsChangedWhileDispatchPrevented
= false
;
969 onItemsChanged(true
);
974 * Called by {@link MenuItemImpl} when its visible flag is changed.
975 * @param item The item that has gone through a visibility change.
977 void onItemVisibleChanged(MenuItemImpl item
) {
978 // Notify of items being changed
979 mIsVisibleItemsStale
= true
;
980 onItemsChanged(true
);
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.
987 void onItemActionRequestChanged(MenuItemImpl item
) {
988 // Notify of items being changed
989 mIsActionItemsStale
= true
;
990 onItemsChanged(true
);
993 ArrayList
<MenuItemImpl
> getVisibleItems() {
994 if (!mIsVisibleItemsStale
) return mVisibleItems
;
996 // Refresh the visible items
997 mVisibleItems
.clear();
999 final int itemsSize
= mItems
.size();
1001 for (int i
= 0; i
< itemsSize
; i
++) {
1002 item
= mItems
.get(i
);
1003 if (item
.isVisible()) mVisibleItems
.add(item
);
1006 mIsVisibleItemsStale
= false
;
1007 mIsActionItemsStale
= true
;
1009 return mVisibleItems
;
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:
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.
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.
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.
1036 public void flagActionItems() {
1037 if (!mIsActionItemsStale
) {
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
);
1048 flagged
|= presenter
.flagActionItems();
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
);
1062 mNonActionItems
.add(item
);
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());
1072 mIsActionItemsStale
= false
;
1075 ArrayList
<MenuItemImpl
> getActionItems() {
1077 return mActionItems
;
1080 ArrayList
<MenuItemImpl
> getNonActionItems() {
1082 return mNonActionItems
;
1085 public void clearHeader() {
1087 mHeaderTitle
= null
;
1090 onItemsChanged(false
);
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();
1100 // If using a custom view, then the title and icon aren't used
1101 mHeaderTitle
= null
;
1105 mHeaderTitle
= r
.getText(titleRes
);
1106 } else if (title
!= null
) {
1107 mHeaderTitle
= title
;
1111 mHeaderIcon
= r
.getDrawable(iconRes
);
1112 } else if (icon
!= null
) {
1116 // If using the title or icon, then a custom view isn't used
1121 onItemsChanged(false
);
1125 * Sets the header's title. This replaces the header view. Called by the
1126 * builder-style methods of subclasses.
1128 * @param title The new title.
1129 * @return This MenuBuilder so additional setters can be called.
1131 protected MenuBuilder
setHeaderTitleInt(CharSequence title
) {
1132 setHeaderInternal(0, title
, 0, null
, null
);
1137 * Sets the header's title. This replaces the header view. Called by the
1138 * builder-style methods of subclasses.
1140 * @param titleRes The new title (as a resource ID).
1141 * @return This MenuBuilder so additional setters can be called.
1143 protected MenuBuilder
setHeaderTitleInt(int titleRes
) {
1144 setHeaderInternal(titleRes
, null
, 0, null
, null
);
1149 * Sets the header's icon. This replaces the header view. Called by the
1150 * builder-style methods of subclasses.
1152 * @param icon The new icon.
1153 * @return This MenuBuilder so additional setters can be called.
1155 protected MenuBuilder
setHeaderIconInt(Drawable icon
) {
1156 setHeaderInternal(0, null
, 0, icon
, null
);
1161 * Sets the header's icon. This replaces the header view. Called by the
1162 * builder-style methods of subclasses.
1164 * @param iconRes The new icon (as a resource ID).
1165 * @return This MenuBuilder so additional setters can be called.
1167 protected MenuBuilder
setHeaderIconInt(int iconRes
) {
1168 setHeaderInternal(0, null
, iconRes
, null
, null
);
1173 * Sets the header's view. This replaces the title and icon. Called by the
1174 * builder-style methods of subclasses.
1176 * @param view The new view.
1177 * @return This MenuBuilder so additional setters can be called.
1179 protected MenuBuilder
setHeaderViewInt(View view
) {
1180 setHeaderInternal(0, null
, 0, null
, view
);
1184 public CharSequence
getHeaderTitle() {
1185 return mHeaderTitle
;
1188 public Drawable
getHeaderIcon() {
1192 public View
getHeaderView() {
1197 * Gets the root menu (if this is a submenu, find its root menu).
1198 * @return The root menu.
1200 public MenuBuilder
getRootMenu() {
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).
1209 * @param menuInfo The extra menu information to add.
1211 public void setCurrentMenuInfo(ContextMenuInfo menuInfo
) {
1212 mCurrentMenuInfo
= menuInfo
;
1215 void setOptionalIconsVisible(boolean visible
) {
1216 mOptionalIconsVisible
= visible
;
1219 boolean getOptionalIconsVisible() {
1220 return mOptionalIconsVisible
;
1223 public boolean expandItemActionView(MenuItemImpl item
) {
1224 if (mPresenters
.isEmpty()) return false
;
1226 boolean expanded
= false
;
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
))) {
1237 startDispatchingItemsChanged();
1240 mExpandedItem
= item
;
1245 public boolean collapseItemActionView(MenuItemImpl item
) {
1246 if (mPresenters
.isEmpty() || mExpandedItem
!= item
) return false
;
1248 boolean collapsed
= false
;
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
))) {
1259 startDispatchingItemsChanged();
1262 mExpandedItem
= null
;
1267 public MenuItemImpl
getExpandedItem() {
1268 return mExpandedItem
;
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) {
1277 boolean visible
= false
;
1279 for (MenuItemImpl nonActionItem
: nonActionItems
) {
1280 if (!nonActionItem
.isVisible()) {
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());
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());
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());
1305 if (subItem
.isExclusiveCheckable()) {
1306 nativeSub
.setGroupCheckable(subItem
.getGroupId(), true
, true
);
1309 map
.put(nativeSubItem
, subItem
);
1312 nativeItem
= nativeSub
.getItem();
1314 nativeItem
= menu
.add(nonActionItem
.getGroupId(), nonActionItem
.getItemId(),
1315 nonActionItem
.getOrder(), nonActionItem
.getTitle());
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());
1327 if (nonActionItem
.isExclusiveCheckable()) {
1328 menu
.setGroupCheckable(nonActionItem
.getGroupId(), true
, true
);
1331 map
.put(nativeItem
, nonActionItem
);