diff options
Diffstat (limited to 'com_actionbarsherlock/src/com/actionbarsherlock/internal/view/menu/MenuBuilder.java')
-rw-r--r-- | com_actionbarsherlock/src/com/actionbarsherlock/internal/view/menu/MenuBuilder.java | 1335 |
1 files changed, 1335 insertions, 0 deletions
diff --git a/com_actionbarsherlock/src/com/actionbarsherlock/internal/view/menu/MenuBuilder.java b/com_actionbarsherlock/src/com/actionbarsherlock/internal/view/menu/MenuBuilder.java new file mode 100644 index 000000000..179b8f037 --- /dev/null +++ b/com_actionbarsherlock/src/com/actionbarsherlock/internal/view/menu/MenuBuilder.java @@ -0,0 +1,1335 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.actionbarsherlock.internal.view.menu; + + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.SparseArray; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; + +import com.actionbarsherlock.R; +import com.actionbarsherlock.view.ActionProvider; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; +import com.actionbarsherlock.view.SubMenu; + +/** + * Implementation of the {@link android.view.Menu} interface for creating a + * standard menu UI. + */ +public class MenuBuilder implements Menu { + //UNUSED private static final String TAG = "MenuBuilder"; + + private static final String PRESENTER_KEY = "android:menu:presenters"; + private static final String ACTION_VIEW_STATES_KEY = "android:menu:actionviewstates"; + private static final String EXPANDED_ACTION_VIEW_ID = "android:menu:expandedactionview"; + + private static final int[] sCategoryToOrder = new int[] { + 1, /* No category */ + 4, /* CONTAINER */ + 5, /* SYSTEM */ + 3, /* SECONDARY */ + 2, /* ALTERNATIVE */ + 0, /* SELECTED_ALTERNATIVE */ + }; + + private final Context mContext; + private final Resources mResources; + + /** + * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode() + * instead of accessing this directly. + */ + private boolean mQwertyMode; + + /** + * Whether the shortcuts should be visible on menus. Use isShortcutsVisible() + * instead of accessing this directly. + */ + private boolean mShortcutsVisible; + + /** + * Callback that will receive the various menu-related events generated by + * this class. Use getCallback to get a reference to the callback. + */ + private Callback mCallback; + + /** Contains all of the items for this menu */ + private ArrayList<MenuItemImpl> mItems; + + /** Contains only the items that are currently visible. This will be created/refreshed from + * {@link #getVisibleItems()} */ + private ArrayList<MenuItemImpl> mVisibleItems; + /** + * Whether or not the items (or any one item's shown state) has changed since it was last + * fetched from {@link #getVisibleItems()} + */ + private boolean mIsVisibleItemsStale; + + /** + * Contains only the items that should appear in the Action Bar, if present. + */ + private ArrayList<MenuItemImpl> mActionItems; + /** + * Contains items that should NOT appear in the Action Bar, if present. + */ + private ArrayList<MenuItemImpl> mNonActionItems; + + /** + * Whether or not the items (or any one item's action state) has changed since it was + * last fetched. + */ + private boolean mIsActionItemsStale; + + /** + * Default value for how added items should show in the action list. + */ + private int mDefaultShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER; + + /** + * Current use case is Context Menus: As Views populate the context menu, each one has + * extra information that should be passed along. This is the current menu info that + * should be set on all items added to this menu. + */ + private ContextMenuInfo mCurrentMenuInfo; + + /** Header title for menu types that have a header (context and submenus) */ + CharSequence mHeaderTitle; + /** Header icon for menu types that have a header and support icons (context) */ + Drawable mHeaderIcon; + /** Header custom view for menu types that have a header and support custom views (context) */ + View mHeaderView; + + /** + * Contains the state of the View hierarchy for all menu views when the menu + * was frozen. + */ + //UNUSED private SparseArray<Parcelable> mFrozenViewStates; + + /** + * Prevents onItemsChanged from doing its junk, useful for batching commands + * that may individually call onItemsChanged. + */ + private boolean mPreventDispatchingItemsChanged = false; + private boolean mItemsChangedWhileDispatchPrevented = false; + + private boolean mOptionalIconsVisible = false; + + private boolean mIsClosing = false; + + private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>(); + + private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters = + new CopyOnWriteArrayList<WeakReference<MenuPresenter>>(); + + /** + * Currently expanded menu item; must be collapsed when we clear. + */ + private MenuItemImpl mExpandedItem; + + /** + * Called by menu to notify of close and selection changes. + */ + public interface Callback { + /** + * Called when a menu item is selected. + * @param menu The menu that is the parent of the item + * @param item The menu item that is selected + * @return whether the menu item selection was handled + */ + public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item); + + /** + * Called when the mode of the menu changes (for example, from icon to expanded). + * + * @param menu the menu that has changed modes + */ + public void onMenuModeChange(MenuBuilder menu); + } + + /** + * Called by menu items to execute their associated action + */ + public interface ItemInvoker { + public boolean invokeItem(MenuItemImpl item); + } + + public MenuBuilder(Context context) { + mContext = context; + mResources = context.getResources(); + + mItems = new ArrayList<MenuItemImpl>(); + + mVisibleItems = new ArrayList<MenuItemImpl>(); + mIsVisibleItemsStale = true; + + mActionItems = new ArrayList<MenuItemImpl>(); + mNonActionItems = new ArrayList<MenuItemImpl>(); + mIsActionItemsStale = true; + + setShortcutsVisibleInner(true); + } + + public MenuBuilder setDefaultShowAsAction(int defaultShowAsAction) { + mDefaultShowAsAction = defaultShowAsAction; + return this; + } + + /** + * Add a presenter to this menu. This will only hold a WeakReference; + * you do not need to explicitly remove a presenter, but you can using + * {@link #removeMenuPresenter(MenuPresenter)}. + * + * @param presenter The presenter to add + */ + public void addMenuPresenter(MenuPresenter presenter) { + mPresenters.add(new WeakReference<MenuPresenter>(presenter)); + presenter.initForMenu(mContext, this); + mIsActionItemsStale = true; + } + + /** + * Remove a presenter from this menu. That presenter will no longer + * receive notifications of updates to this menu's data. + * + * @param presenter The presenter to remove + */ + public void removeMenuPresenter(MenuPresenter presenter) { + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter item = ref.get(); + if (item == null || item == presenter) { + mPresenters.remove(ref); + } + } + } + + private void dispatchPresenterUpdate(boolean cleared) { + if (mPresenters.isEmpty()) return; + + stopDispatchingItemsChanged(); + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else { + presenter.updateMenuView(cleared); + } + } + startDispatchingItemsChanged(); + } + + private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu) { + if (mPresenters.isEmpty()) return false; + + boolean result = false; + + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else if (!result) { + result = presenter.onSubMenuSelected(subMenu); + } + } + return result; + } + + private void dispatchSaveInstanceState(Bundle outState) { + if (mPresenters.isEmpty()) return; + + SparseArray<Parcelable> presenterStates = new SparseArray<Parcelable>(); + + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else { + final int id = presenter.getId(); + if (id > 0) { + final Parcelable state = presenter.onSaveInstanceState(); + if (state != null) { + presenterStates.put(id, state); + } + } + } + } + + outState.putSparseParcelableArray(PRESENTER_KEY, presenterStates); + } + + private void dispatchRestoreInstanceState(Bundle state) { + SparseArray<Parcelable> presenterStates = state.getSparseParcelableArray(PRESENTER_KEY); + + if (presenterStates == null || mPresenters.isEmpty()) return; + + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else { + final int id = presenter.getId(); + if (id > 0) { + Parcelable parcel = presenterStates.get(id); + if (parcel != null) { + presenter.onRestoreInstanceState(parcel); + } + } + } + } + } + + public void savePresenterStates(Bundle outState) { + dispatchSaveInstanceState(outState); + } + + public void restorePresenterStates(Bundle state) { + dispatchRestoreInstanceState(state); + } + + public void saveActionViewStates(Bundle outStates) { + SparseArray<Parcelable> viewStates = null; + + final int itemCount = size(); + for (int i = 0; i < itemCount; i++) { + final MenuItem item = getItem(i); + final View v = item.getActionView(); + if (v != null && v.getId() != View.NO_ID) { + if (viewStates == null) { + viewStates = new SparseArray<Parcelable>(); + } + v.saveHierarchyState(viewStates); + if (item.isActionViewExpanded()) { + outStates.putInt(EXPANDED_ACTION_VIEW_ID, item.getItemId()); + } + } + if (item.hasSubMenu()) { + final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu(); + subMenu.saveActionViewStates(outStates); + } + } + + if (viewStates != null) { + outStates.putSparseParcelableArray(getActionViewStatesKey(), viewStates); + } + } + + public void restoreActionViewStates(Bundle states) { + if (states == null) { + return; + } + + SparseArray<Parcelable> viewStates = states.getSparseParcelableArray( + getActionViewStatesKey()); + + final int itemCount = size(); + for (int i = 0; i < itemCount; i++) { + final MenuItem item = getItem(i); + final View v = item.getActionView(); + if (v != null && v.getId() != View.NO_ID) { + v.restoreHierarchyState(viewStates); + } + if (item.hasSubMenu()) { + final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu(); + subMenu.restoreActionViewStates(states); + } + } + + final int expandedId = states.getInt(EXPANDED_ACTION_VIEW_ID); + if (expandedId > 0) { + MenuItem itemToExpand = findItem(expandedId); + if (itemToExpand != null) { + itemToExpand.expandActionView(); + } + } + } + + protected String getActionViewStatesKey() { + return ACTION_VIEW_STATES_KEY; + } + + public void setCallback(Callback cb) { + mCallback = cb; + } + + /** + * Adds an item to the menu. The other add methods funnel to this. + */ + private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) { + final int ordering = getOrdering(categoryOrder); + + final MenuItemImpl item = new MenuItemImpl(this, group, id, categoryOrder, + ordering, title, mDefaultShowAsAction); + + if (mCurrentMenuInfo != null) { + // Pass along the current menu info + item.setMenuInfo(mCurrentMenuInfo); + } + + mItems.add(findInsertIndex(mItems, ordering), item); + onItemsChanged(true); + + return item; + } + + public MenuItem add(CharSequence title) { + return addInternal(0, 0, 0, title); + } + + public MenuItem add(int titleRes) { + return addInternal(0, 0, 0, mResources.getString(titleRes)); + } + + public MenuItem add(int group, int id, int categoryOrder, CharSequence title) { + return addInternal(group, id, categoryOrder, title); + } + + public MenuItem add(int group, int id, int categoryOrder, int title) { + return addInternal(group, id, categoryOrder, mResources.getString(title)); + } + + public SubMenu addSubMenu(CharSequence title) { + return addSubMenu(0, 0, 0, title); + } + + public SubMenu addSubMenu(int titleRes) { + return addSubMenu(0, 0, 0, mResources.getString(titleRes)); + } + + public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) { + final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title); + final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item); + item.setSubMenu(subMenu); + + return subMenu; + } + + public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) { + return addSubMenu(group, id, categoryOrder, mResources.getString(title)); + } + + public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller, + Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) { + PackageManager pm = mContext.getPackageManager(); + final List<ResolveInfo> lri = + pm.queryIntentActivityOptions(caller, specifics, intent, 0); + final int N = lri != null ? lri.size() : 0; + + if ((flags & FLAG_APPEND_TO_GROUP) == 0) { + removeGroup(group); + } + + for (int i=0; i<N; i++) { + final ResolveInfo ri = lri.get(i); + Intent rintent = new Intent( + ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]); + rintent.setComponent(new ComponentName( + ri.activityInfo.applicationInfo.packageName, + ri.activityInfo.name)); + final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm)) + .setIcon(ri.loadIcon(pm)) + .setIntent(rintent); + if (outSpecificItems != null && ri.specificIndex >= 0) { + outSpecificItems[ri.specificIndex] = item; + } + } + + return N; + } + + public void removeItem(int id) { + removeItemAtInt(findItemIndex(id), true); + } + + public void removeGroup(int group) { + final int i = findGroupIndex(group); + + if (i >= 0) { + final int maxRemovable = mItems.size() - i; + int numRemoved = 0; + while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) { + // Don't force update for each one, this method will do it at the end + removeItemAtInt(i, false); + } + + // Notify menu views + onItemsChanged(true); + } + } + + /** + * Remove the item at the given index and optionally forces menu views to + * update. + * + * @param index The index of the item to be removed. If this index is + * invalid an exception is thrown. + * @param updateChildrenOnMenuViews Whether to force update on menu views. + * Please make sure you eventually call this after your batch of + * removals. + */ + private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) { + if ((index < 0) || (index >= mItems.size())) return; + + mItems.remove(index); + + if (updateChildrenOnMenuViews) onItemsChanged(true); + } + + public void removeItemAt(int index) { + removeItemAtInt(index, true); + } + + public void clearAll() { + mPreventDispatchingItemsChanged = true; + clear(); + clearHeader(); + mPreventDispatchingItemsChanged = false; + mItemsChangedWhileDispatchPrevented = false; + onItemsChanged(true); + } + + public void clear() { + if (mExpandedItem != null) { + collapseItemActionView(mExpandedItem); + } + mItems.clear(); + + onItemsChanged(true); + } + + void setExclusiveItemChecked(MenuItem item) { + final int group = item.getGroupId(); + + final int N = mItems.size(); + for (int i = 0; i < N; i++) { + MenuItemImpl curItem = mItems.get(i); + if (curItem.getGroupId() == group) { + if (!curItem.isExclusiveCheckable()) continue; + if (!curItem.isCheckable()) continue; + + // Check the item meant to be checked, uncheck the others (that are in the group) + curItem.setCheckedInt(curItem == item); + } + } + } + + public void setGroupCheckable(int group, boolean checkable, boolean exclusive) { + final int N = mItems.size(); + + for (int i = 0; i < N; i++) { + MenuItemImpl item = mItems.get(i); + if (item.getGroupId() == group) { + item.setExclusiveCheckable(exclusive); + item.setCheckable(checkable); + } + } + } + + public void setGroupVisible(int group, boolean visible) { + final int N = mItems.size(); + + // We handle the notification of items being changed ourselves, so we use setVisibleInt rather + // than setVisible and at the end notify of items being changed + + boolean changedAtLeastOneItem = false; + for (int i = 0; i < N; i++) { + MenuItemImpl item = mItems.get(i); + if (item.getGroupId() == group) { + if (item.setVisibleInt(visible)) changedAtLeastOneItem = true; + } + } + + if (changedAtLeastOneItem) onItemsChanged(true); + } + + public void setGroupEnabled(int group, boolean enabled) { + final int N = mItems.size(); + + for (int i = 0; i < N; i++) { + MenuItemImpl item = mItems.get(i); + if (item.getGroupId() == group) { + item.setEnabled(enabled); + } + } + } + + public boolean hasVisibleItems() { + final int size = size(); + + for (int i = 0; i < size; i++) { + MenuItemImpl item = mItems.get(i); + if (item.isVisible()) { + return true; + } + } + + return false; + } + + public MenuItem findItem(int id) { + final int size = size(); + for (int i = 0; i < size; i++) { + MenuItemImpl item = mItems.get(i); + if (item.getItemId() == id) { + return item; + } else if (item.hasSubMenu()) { + MenuItem possibleItem = item.getSubMenu().findItem(id); + + if (possibleItem != null) { + return possibleItem; + } + } + } + + return null; + } + + public int findItemIndex(int id) { + final int size = size(); + + for (int i = 0; i < size; i++) { + MenuItemImpl item = mItems.get(i); + if (item.getItemId() == id) { + return i; + } + } + + return -1; + } + + public int findGroupIndex(int group) { + return findGroupIndex(group, 0); + } + + public int findGroupIndex(int group, int start) { + final int size = size(); + + if (start < 0) { + start = 0; + } + + for (int i = start; i < size; i++) { + final MenuItemImpl item = mItems.get(i); + + if (item.getGroupId() == group) { + return i; + } + } + + return -1; + } + + public int size() { + return mItems.size(); + } + + /** {@inheritDoc} */ + public MenuItem getItem(int index) { + return mItems.get(index); + } + + public boolean isShortcutKey(int keyCode, KeyEvent event) { + return findItemWithShortcutForKey(keyCode, event) != null; + } + + public void setQwertyMode(boolean isQwerty) { + mQwertyMode = isQwerty; + + onItemsChanged(false); + } + + /** + * Returns the ordering across all items. This will grab the category from + * the upper bits, find out how to order the category with respect to other + * categories, and combine it with the lower bits. + * + * @param categoryOrder The category order for a particular item (if it has + * not been or/add with a category, the default category is + * assumed). + * @return An ordering integer that can be used to order this item across + * all the items (even from other categories). + */ + private static int getOrdering(int categoryOrder) { + final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT; + + if (index < 0 || index >= sCategoryToOrder.length) { + throw new IllegalArgumentException("order does not contain a valid category."); + } + + return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK); + } + + /** + * @return whether the menu shortcuts are in qwerty mode or not + */ + boolean isQwertyMode() { + return mQwertyMode; + } + + /** + * Sets whether the shortcuts should be visible on menus. Devices without hardware + * key input will never make shortcuts visible even if this method is passed 'true'. + * + * @param shortcutsVisible Whether shortcuts should be visible (if true and a + * menu item does not have a shortcut defined, that item will + * still NOT show a shortcut) + */ + public void setShortcutsVisible(boolean shortcutsVisible) { + if (mShortcutsVisible == shortcutsVisible) return; + + setShortcutsVisibleInner(shortcutsVisible); + onItemsChanged(false); + } + + private void setShortcutsVisibleInner(boolean shortcutsVisible) { + mShortcutsVisible = shortcutsVisible + && mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS + && mResources.getBoolean( + R.bool.abs__config_showMenuShortcutsWhenKeyboardPresent); + } + + /** + * @return Whether shortcuts should be visible on menus. + */ + public boolean isShortcutsVisible() { + return mShortcutsVisible; + } + + Resources getResources() { + return mResources; + } + + public Context getContext() { + return mContext; + } + + boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) { + return mCallback != null && mCallback.onMenuItemSelected(menu, item); + } + + /** + * Dispatch a mode change event to this menu's callback. + */ + public void changeMenuMode() { + if (mCallback != null) { + mCallback.onMenuModeChange(this); + } + } + + private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) { + for (int i = items.size() - 1; i >= 0; i--) { + MenuItemImpl item = items.get(i); + if (item.getOrdering() <= ordering) { + return i + 1; + } + } + + return 0; + } + + public boolean performShortcut(int keyCode, KeyEvent event, int flags) { + final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event); + + boolean handled = false; + + if (item != null) { + handled = performItemAction(item, flags); + } + + if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) { + close(true); + } + + return handled; + } + + /* + * This function will return all the menu and sub-menu items that can + * be directly (the shortcut directly corresponds) and indirectly + * (the ALT-enabled char corresponds to the shortcut) associated + * with the keyCode. + */ + @SuppressWarnings("deprecation") + void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) { + final boolean qwerty = isQwertyMode(); + final int metaState = event.getMetaState(); + final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); + // Get the chars associated with the keyCode (i.e using any chording combo) + final boolean isKeyCodeMapped = event.getKeyData(possibleChars); + // The delete key is not mapped to '\b' so we treat it specially + if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) { + return; + } + + // Look for an item whose shortcut is this key. + final int N = mItems.size(); + for (int i = 0; i < N; i++) { + MenuItemImpl item = mItems.get(i); + if (item.hasSubMenu()) { + ((MenuBuilder)item.getSubMenu()).findItemsWithShortcutForKey(items, keyCode, event); + } + final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut(); + if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) && + (shortcutChar != 0) && + (shortcutChar == possibleChars.meta[0] + || shortcutChar == possibleChars.meta[2] + || (qwerty && shortcutChar == '\b' && + keyCode == KeyEvent.KEYCODE_DEL)) && + item.isEnabled()) { + items.add(item); + } + } + } + + /* + * We want to return the menu item associated with the key, but if there is no + * ambiguity (i.e. there is only one menu item corresponding to the key) we want + * to return it even if it's not an exact match; this allow the user to + * _not_ use the ALT key for example, making the use of shortcuts slightly more + * user-friendly. An example is on the G1, '!' and '1' are on the same key, and + * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut). + * + * On the other hand, if two (or more) shortcuts corresponds to the same key, + * we have to only return the exact match. + */ + @SuppressWarnings("deprecation") + MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) { + // Get all items that can be associated directly or indirectly with the keyCode + ArrayList<MenuItemImpl> items = mTempShortcutItemList; + items.clear(); + findItemsWithShortcutForKey(items, keyCode, event); + + if (items.isEmpty()) { + return null; + } + + final int metaState = event.getMetaState(); + final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); + // Get the chars associated with the keyCode (i.e using any chording combo) + event.getKeyData(possibleChars); + + // If we have only one element, we can safely returns it + final int size = items.size(); + if (size == 1) { + return items.get(0); + } + + final boolean qwerty = isQwertyMode(); + // If we found more than one item associated with the key, + // we have to return the exact match + for (int i = 0; i < size; i++) { + final MenuItemImpl item = items.get(i); + final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : + item.getNumericShortcut(); + if ((shortcutChar == possibleChars.meta[0] && + (metaState & KeyEvent.META_ALT_ON) == 0) + || (shortcutChar == possibleChars.meta[2] && + (metaState & KeyEvent.META_ALT_ON) != 0) + || (qwerty && shortcutChar == '\b' && + keyCode == KeyEvent.KEYCODE_DEL)) { + return item; + } + } + return null; + } + + public boolean performIdentifierAction(int id, int flags) { + // Look for an item whose identifier is the id. + return performItemAction(findItem(id), flags); + } + + public boolean performItemAction(MenuItem item, int flags) { + MenuItemImpl itemImpl = (MenuItemImpl) item; + + if (itemImpl == null || !itemImpl.isEnabled()) { + return false; + } + + boolean invoked = itemImpl.invoke(); + + if (itemImpl.hasCollapsibleActionView()) { + invoked |= itemImpl.expandActionView(); + if (invoked) close(true); + } else if (item.hasSubMenu()) { + close(false); + + final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu(); + final ActionProvider provider = item.getActionProvider(); + if (provider != null && provider.hasSubMenu()) { + provider.onPrepareSubMenu(subMenu); + } + invoked |= dispatchSubMenuSelected(subMenu); + if (!invoked) close(true); + } else { + if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) { + close(true); + } + } + + return invoked; + } + + /** + * Closes the visible menu. + * + * @param allMenusAreClosing Whether the menus are completely closing (true), + * or whether there is another menu coming in this menu's place + * (false). For example, if the menu is closing because a + * sub menu is about to be shown, <var>allMenusAreClosing</var> + * is false. + */ + final void close(boolean allMenusAreClosing) { + if (mIsClosing) return; + + mIsClosing = true; + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else { + presenter.onCloseMenu(this, allMenusAreClosing); + } + } + mIsClosing = false; + } + + /** {@inheritDoc} */ + public void close() { + close(true); + } + + /** + * Called when an item is added or removed. + * + * @param structureChanged true if the menu structure changed, + * false if only item properties changed. + * (Visibility is a structural property since it affects layout.) + */ + void onItemsChanged(boolean structureChanged) { + if (!mPreventDispatchingItemsChanged) { + if (structureChanged) { + mIsVisibleItemsStale = true; + mIsActionItemsStale = true; + } + + dispatchPresenterUpdate(structureChanged); + } else { + mItemsChangedWhileDispatchPrevented = true; + } + } + + /** + * Stop dispatching item changed events to presenters until + * {@link #startDispatchingItemsChanged()} is called. Useful when + * many menu operations are going to be performed as a batch. + */ + public void stopDispatchingItemsChanged() { + if (!mPreventDispatchingItemsChanged) { + mPreventDispatchingItemsChanged = true; + mItemsChangedWhileDispatchPrevented = false; + } + } + + public void startDispatchingItemsChanged() { + mPreventDispatchingItemsChanged = false; + + if (mItemsChangedWhileDispatchPrevented) { + mItemsChangedWhileDispatchPrevented = false; + onItemsChanged(true); + } + } + + /** + * Called by {@link MenuItemImpl} when its visible flag is changed. + * @param item The item that has gone through a visibility change. + */ + void onItemVisibleChanged(MenuItemImpl item) { + // Notify of items being changed + mIsVisibleItemsStale = true; + onItemsChanged(true); + } + + /** + * Called by {@link MenuItemImpl} when its action request status is changed. + * @param item The item that has gone through a change in action request status. + */ + void onItemActionRequestChanged(MenuItemImpl item) { + // Notify of items being changed + mIsActionItemsStale = true; + onItemsChanged(true); + } + + ArrayList<MenuItemImpl> getVisibleItems() { + if (!mIsVisibleItemsStale) return mVisibleItems; + + // Refresh the visible items + mVisibleItems.clear(); + + final int itemsSize = mItems.size(); + MenuItemImpl item; + for (int i = 0; i < itemsSize; i++) { + item = mItems.get(i); + if (item.isVisible()) mVisibleItems.add(item); + } + + mIsVisibleItemsStale = false; + mIsActionItemsStale = true; + + return mVisibleItems; + } + + /** + * This method determines which menu items get to be 'action items' that will appear + * in an action bar and which items should be 'overflow items' in a secondary menu. + * The rules are as follows: + * + * <p>Items are considered for inclusion in the order specified within the menu. + * There is a limit of mMaxActionItems as a total count, optionally including the overflow + * menu button itself. This is a soft limit; if an item shares a group ID with an item + * previously included as an action item, the new item will stay with its group and become + * an action item itself even if it breaks the max item count limit. This is done to + * limit the conceptual complexity of the items presented within an action bar. Only a few + * unrelated concepts should be presented to the user in this space, and groups are treated + * as a single concept. + * + * <p>There is also a hard limit of consumed measurable space: mActionWidthLimit. This + * limit may be broken by a single item that exceeds the remaining space, but no further + * items may be added. If an item that is part of a group cannot fit within the remaining + * measured width, the entire group will be demoted to overflow. This is done to ensure room + * for navigation and other affordances in the action bar as well as reduce general UI clutter. + * + * <p>The space freed by demoting a full group cannot be consumed by future menu items. + * Once items begin to overflow, all future items become overflow items as well. This is + * to avoid inadvertent reordering that may break the app's intended design. + */ + public void flagActionItems() { + if (!mIsActionItemsStale) { + return; + } + + // Presenters flag action items as needed. + boolean flagged = false; + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else { + flagged |= presenter.flagActionItems(); + } + } + + if (flagged) { + mActionItems.clear(); + mNonActionItems.clear(); + ArrayList<MenuItemImpl> visibleItems = getVisibleItems(); + final int itemsSize = visibleItems.size(); + for (int i = 0; i < itemsSize; i++) { + MenuItemImpl item = visibleItems.get(i); + if (item.isActionButton()) { + mActionItems.add(item); + } else { + mNonActionItems.add(item); + } + } + } else { + // Nobody flagged anything, everything is a non-action item. + // (This happens during a first pass with no action-item presenters.) + mActionItems.clear(); + mNonActionItems.clear(); + mNonActionItems.addAll(getVisibleItems()); + } + mIsActionItemsStale = false; + } + + ArrayList<MenuItemImpl> getActionItems() { + flagActionItems(); + return mActionItems; + } + + ArrayList<MenuItemImpl> getNonActionItems() { + flagActionItems(); + return mNonActionItems; + } + + public void clearHeader() { + mHeaderIcon = null; + mHeaderTitle = null; + mHeaderView = null; + + onItemsChanged(false); + } + + private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes, + final Drawable icon, final View view) { + final Resources r = getResources(); + + if (view != null) { + mHeaderView = view; + + // If using a custom view, then the title and icon aren't used + mHeaderTitle = null; + mHeaderIcon = null; + } else { + if (titleRes > 0) { + mHeaderTitle = r.getText(titleRes); + } else if (title != null) { + mHeaderTitle = title; + } + + if (iconRes > 0) { + mHeaderIcon = r.getDrawable(iconRes); + } else if (icon != null) { + mHeaderIcon = icon; + } + + // If using the title or icon, then a custom view isn't used + mHeaderView = null; + } + + // Notify of change + onItemsChanged(false); + } + + /** + * Sets the header's title. This replaces the header view. Called by the + * builder-style methods of subclasses. + * + * @param title The new title. + * @return This MenuBuilder so additional setters can be called. + */ + protected MenuBuilder setHeaderTitleInt(CharSequence title) { + setHeaderInternal(0, title, 0, null, null); + return this; + } + + /** + * Sets the header's title. This replaces the header view. Called by the + * builder-style methods of subclasses. + * + * @param titleRes The new title (as a resource ID). + * @return This MenuBuilder so additional setters can be called. + */ + protected MenuBuilder setHeaderTitleInt(int titleRes) { + setHeaderInternal(titleRes, null, 0, null, null); + return this; + } + + /** + * Sets the header's icon. This replaces the header view. Called by the + * builder-style methods of subclasses. + * + * @param icon The new icon. + * @return This MenuBuilder so additional setters can be called. + */ + protected MenuBuilder setHeaderIconInt(Drawable icon) { + setHeaderInternal(0, null, 0, icon, null); + return this; + } + + /** + * Sets the header's icon. This replaces the header view. Called by the + * builder-style methods of subclasses. + * + * @param iconRes The new icon (as a resource ID). + * @return This MenuBuilder so additional setters can be called. + */ + protected MenuBuilder setHeaderIconInt(int iconRes) { + setHeaderInternal(0, null, iconRes, null, null); + return this; + } + + /** + * Sets the header's view. This replaces the title and icon. Called by the + * builder-style methods of subclasses. + * + * @param view The new view. + * @return This MenuBuilder so additional setters can be called. + */ + protected MenuBuilder setHeaderViewInt(View view) { + setHeaderInternal(0, null, 0, null, view); + return this; + } + + public CharSequence getHeaderTitle() { + return mHeaderTitle; + } + + public Drawable getHeaderIcon() { + return mHeaderIcon; + } + + public View getHeaderView() { + return mHeaderView; + } + + /** + * Gets the root menu (if this is a submenu, find its root menu). + * @return The root menu. + */ + public MenuBuilder getRootMenu() { + return this; + } + + /** + * Sets the current menu info that is set on all items added to this menu + * (until this is called again with different menu info, in which case that + * one will be added to all subsequent item additions). + * + * @param menuInfo The extra menu information to add. + */ + public void setCurrentMenuInfo(ContextMenuInfo menuInfo) { + mCurrentMenuInfo = menuInfo; + } + + void setOptionalIconsVisible(boolean visible) { + mOptionalIconsVisible = visible; + } + + boolean getOptionalIconsVisible() { + return mOptionalIconsVisible; + } + + public boolean expandItemActionView(MenuItemImpl item) { + if (mPresenters.isEmpty()) return false; + + boolean expanded = false; + + stopDispatchingItemsChanged(); + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else if ((expanded = presenter.expandItemActionView(this, item))) { + break; + } + } + startDispatchingItemsChanged(); + + if (expanded) { + mExpandedItem = item; + } + return expanded; + } + + public boolean collapseItemActionView(MenuItemImpl item) { + if (mPresenters.isEmpty() || mExpandedItem != item) return false; + + boolean collapsed = false; + + stopDispatchingItemsChanged(); + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else if ((collapsed = presenter.collapseItemActionView(this, item))) { + break; + } + } + startDispatchingItemsChanged(); + + if (collapsed) { + mExpandedItem = null; + } + return collapsed; + } + + public MenuItemImpl getExpandedItem() { + return mExpandedItem; + } + + public boolean bindNativeOverflow(android.view.Menu menu, android.view.MenuItem.OnMenuItemClickListener listener, HashMap<android.view.MenuItem, MenuItemImpl> map) { + final List<MenuItemImpl> nonActionItems = getNonActionItems(); + if (nonActionItems == null || nonActionItems.size() == 0) { + return false; + } + + boolean visible = false; + menu.clear(); + for (MenuItemImpl nonActionItem : nonActionItems) { + if (!nonActionItem.isVisible()) { + continue; + } + visible = true; + + android.view.MenuItem nativeItem; + if (nonActionItem.hasSubMenu()) { + android.view.SubMenu nativeSub = menu.addSubMenu(nonActionItem.getGroupId(), nonActionItem.getItemId(), + nonActionItem.getOrder(), nonActionItem.getTitle()); + + SubMenuBuilder subMenu = (SubMenuBuilder)nonActionItem.getSubMenu(); + for (MenuItemImpl subItem : subMenu.getVisibleItems()) { + android.view.MenuItem nativeSubItem = nativeSub.add(subItem.getGroupId(), subItem.getItemId(), + subItem.getOrder(), subItem.getTitle()); + + nativeSubItem.setIcon(subItem.getIcon()); + nativeSubItem.setOnMenuItemClickListener(listener); + nativeSubItem.setEnabled(subItem.isEnabled()); + nativeSubItem.setIntent(subItem.getIntent()); + nativeSubItem.setNumericShortcut(subItem.getNumericShortcut()); + nativeSubItem.setAlphabeticShortcut(subItem.getAlphabeticShortcut()); + nativeSubItem.setTitleCondensed(subItem.getTitleCondensed()); + nativeSubItem.setCheckable(subItem.isCheckable()); + nativeSubItem.setChecked(subItem.isChecked()); + + if (subItem.isExclusiveCheckable()) { + nativeSub.setGroupCheckable(subItem.getGroupId(), true, true); + } + + map.put(nativeSubItem, subItem); + } + + nativeItem = nativeSub.getItem(); + } else { + nativeItem = menu.add(nonActionItem.getGroupId(), nonActionItem.getItemId(), + nonActionItem.getOrder(), nonActionItem.getTitle()); + } + nativeItem.setIcon(nonActionItem.getIcon()); + nativeItem.setOnMenuItemClickListener(listener); + nativeItem.setEnabled(nonActionItem.isEnabled()); + nativeItem.setIntent(nonActionItem.getIntent()); + nativeItem.setNumericShortcut(nonActionItem.getNumericShortcut()); + nativeItem.setAlphabeticShortcut(nonActionItem.getAlphabeticShortcut()); + nativeItem.setTitleCondensed(nonActionItem.getTitleCondensed()); + nativeItem.setCheckable(nonActionItem.isCheckable()); + nativeItem.setChecked(nonActionItem.isChecked()); + + if (nonActionItem.isExclusiveCheckable()) { + menu.setGroupCheckable(nonActionItem.getGroupId(), true, true); + } + + map.put(nativeItem, nonActionItem); + } + return visible; + } +} |