/* * Copyright (C) 2011 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.android.internal.view.menu; import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.util.Log; import android.util.SparseBooleanArray; import android.view.MenuItem; import android.view.SoundEffectConstants; import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.widget.ImageButton; import java.util.ArrayList; /** * MenuPresenter for building action menus as seen in the action bar and action modes. */ public class ActionMenuPresenter extends BaseMenuPresenter { private View mOverflowButton; private boolean mReserveOverflow; private int mWidthLimit; private int mActionItemWidthLimit; private int mMaxItems; // Group IDs that have been added as actions - used temporarily, allocated here for reuse. private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray(); private View mScrapActionButtonView; private OverflowPopup mOverflowPopup; private ActionButtonSubmenu mActionButtonPopup; private OpenOverflowRunnable mPostedOpenRunnable; public ActionMenuPresenter() { super(com.android.internal.R.layout.action_menu_layout, com.android.internal.R.layout.action_menu_item_layout); } @Override public void initForMenu(Context context, MenuBuilder menu) { super.initForMenu(context, menu); final Resources res = context.getResources(); final int screen = res.getConfiguration().screenLayout; // TODO Use the no-buttons specifier instead here mReserveOverflow = (screen & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE; mWidthLimit = res.getDisplayMetrics().widthPixels / 2; // Measure for initial configuration mMaxItems = res.getInteger(com.android.internal.R.integer.max_action_buttons); int width = mWidthLimit; if (mReserveOverflow) { if (mOverflowButton == null) { mOverflowButton = new OverflowMenuButton(mContext); final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); mOverflowButton.measure(spec, spec); } width -= mOverflowButton.getMeasuredWidth(); } else { mOverflowButton = null; } mActionItemWidthLimit = width; // Drop a scrap view as it may no longer reflect the proper context/config. mScrapActionButtonView = null; } public void setWidthLimit(int width) { if (mReserveOverflow) { width -= mOverflowButton.getMeasuredWidth(); } mActionItemWidthLimit = width; } public void setItemLimit(int itemCount) { mMaxItems = itemCount; } @Override public MenuView getMenuView(ViewGroup root) { MenuView result = super.getMenuView(root); ((ActionMenuView) result).setPresenter(this); return result; } @Override public View getItemView(MenuItemImpl item, View convertView, ViewGroup parent) { final View actionView = item.getActionView(); return actionView != null ? actionView : super.getItemView(item, convertView, parent); } @Override public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) { itemView.initialize(item, 0); ((ActionMenuItemView) itemView).setItemInvoker((ActionMenuView) mMenuView); } @Override public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) { return item.isActionButton(); } @Override public void updateMenuView(boolean cleared) { super.updateMenuView(cleared); if (mReserveOverflow && mMenu.getNonActionItems().size() > 0) { if (mOverflowButton == null) { mOverflowButton = new OverflowMenuButton(mContext); } ViewGroup parent = (ViewGroup) mOverflowButton.getParent(); if (parent != mMenuView) { if (parent != null) { parent.removeView(mOverflowButton); } ((ViewGroup) mMenuView).addView(mOverflowButton); } } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) { ((ViewGroup) mMenuView).removeView(mOverflowButton); } } @Override public boolean filterLeftoverView(ViewGroup parent, int childIndex) { if (parent.getChildAt(childIndex) == mOverflowButton) return false; return super.filterLeftoverView(parent, childIndex); } public boolean onSubMenuSelected(SubMenuBuilder subMenu) { if (!subMenu.hasVisibleItems()) return false; SubMenuBuilder topSubMenu = subMenu; while (topSubMenu.getParentMenu() != mMenu) { topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu(); } View anchor = findViewForItem(topSubMenu.getItem()); if (anchor == null) return false; mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu); mActionButtonPopup.setAnchorView(anchor); mActionButtonPopup.show(); super.onSubMenuSelected(subMenu); return true; } private View findViewForItem(MenuItem item) { final ViewGroup parent = (ViewGroup) mMenuView; if (parent == null) return null; final int count = parent.getChildCount(); for (int i = 0; i < count; i++) { final View child = parent.getChildAt(i); if (child instanceof MenuView.ItemView && ((MenuView.ItemView) child).getItemData() == item) { return child; } } return null; } /** * Display the overflow menu if one is present. * @return true if the overflow menu was shown, false otherwise. */ public boolean showOverflowMenu() { if (mReserveOverflow && !isOverflowMenuShowing() && mMenuView != null && mPostedOpenRunnable == null) { Log.d("ActionMenuPresenter", "showOverflowMenu"); OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true); mPostedOpenRunnable = new OpenOverflowRunnable(popup); // Post this for later; we might still need a layout for the anchor to be right. ((View) mMenuView).post(mPostedOpenRunnable); // ActionMenuPresenter uses null as a callback argument here // to indicate overflow is opening. super.onSubMenuSelected(null); return true; } return false; } /** * Hide the overflow menu if it is currently showing. * * @return true if the overflow menu was hidden, false otherwise. */ public boolean hideOverflowMenu() { if (mPostedOpenRunnable != null && mMenuView != null) { ((View) mMenuView).removeCallbacks(mPostedOpenRunnable); return true; } MenuPopupHelper popup = mOverflowPopup; if (popup != null) { popup.dismiss(); return true; } return false; } /** * Dismiss all popup menus - overflow and submenus. * @return true if popups were dismissed, false otherwise. (This can be because none were open.) */ public boolean dismissPopupMenus() { boolean result = hideOverflowMenu(); result |= hideSubMenus(); return result; } /** * Dismiss all submenu popups. * * @return true if popups were dismissed, false otherwise. (This can be because none were open.) */ public boolean hideSubMenus() { if (mActionButtonPopup != null) { mActionButtonPopup.dismiss(); return true; } return false; } /** * @return true if the overflow menu is currently showing */ public boolean isOverflowMenuShowing() { return mOverflowPopup != null && mOverflowPopup.isShowing(); } /** * @return true if space has been reserved in the action menu for an overflow item. */ public boolean isOverflowReserved() { return mReserveOverflow; } public boolean flagActionItems() { final ArrayList visibleItems = mMenu.getVisibleItems(); final int itemsSize = visibleItems.size(); int maxActions = mMaxItems; int widthLimit = mActionItemWidthLimit; final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); final ViewGroup parent = (ViewGroup) mMenuView; int requiredItems = 0; int requestedItems = 0; int firstActionWidth = 0; boolean hasOverflow = false; for (int i = 0; i < itemsSize; i++) { MenuItemImpl item = visibleItems.get(i); if (item.requiresActionButton()) { requiredItems++; } else if (item.requestsActionButton()) { requestedItems++; } else { hasOverflow = true; } } // Reserve a spot for the overflow item if needed. if (mReserveOverflow && (hasOverflow || requiredItems + requestedItems > maxActions)) { maxActions--; } maxActions -= requiredItems; final SparseBooleanArray seenGroups = mActionButtonGroups; seenGroups.clear(); // Flag as many more requested items as will fit. for (int i = 0; i < itemsSize; i++) { MenuItemImpl item = visibleItems.get(i); if (item.requiresActionButton()) { View v = item.getActionView(); if (v == null) { v = getItemView(item, mScrapActionButtonView, parent); if (mScrapActionButtonView == null) { mScrapActionButtonView = v; } } v.measure(querySpec, querySpec); final int measuredWidth = v.getMeasuredWidth(); widthLimit -= measuredWidth; if (firstActionWidth == 0) { firstActionWidth = measuredWidth; } final int groupId = item.getGroupId(); if (groupId != 0) { seenGroups.put(groupId, true); } } else if (item.requestsActionButton()) { // Items in a group with other items that already have an action slot // can break the max actions rule, but not the width limit. final int groupId = item.getGroupId(); final boolean inGroup = seenGroups.get(groupId); boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0; maxActions--; if (isAction) { View v = item.getActionView(); if (v == null) { v = getItemView(item, mScrapActionButtonView, parent); if (mScrapActionButtonView == null) { mScrapActionButtonView = v; } } v.measure(querySpec, querySpec); final int measuredWidth = v.getMeasuredWidth(); widthLimit -= measuredWidth; if (firstActionWidth == 0) { firstActionWidth = measuredWidth; } // Did this push the entire first item past halfway? if (widthLimit + firstActionWidth <= 0) { isAction = false; } } if (isAction && groupId != 0) { seenGroups.put(groupId, true); } else if (inGroup) { // We broke the width limit. Demote the whole group, they all overflow now. seenGroups.put(groupId, false); for (int j = 0; j < i; j++) { MenuItemImpl areYouMyGroupie = visibleItems.get(j); if (areYouMyGroupie.getGroupId() == groupId) { areYouMyGroupie.setIsActionButton(false); } } } item.setIsActionButton(isAction); } } return true; } @Override public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { dismissPopupMenus(); super.onCloseMenu(menu, allMenusAreClosing); } private class OverflowMenuButton extends ImageButton implements ActionMenuChildView { public OverflowMenuButton(Context context) { super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle); setClickable(true); setFocusable(true); setVisibility(VISIBLE); setEnabled(true); } @Override public boolean performClick() { if (super.performClick()) { return true; } playSoundEffect(SoundEffectConstants.CLICK); showOverflowMenu(); return true; } public boolean needsDividerBefore() { return true; } public boolean needsDividerAfter() { return false; } } private class OverflowPopup extends MenuPopupHelper { public OverflowPopup(Context context, MenuBuilder menu, View anchorView, boolean overflowOnly) { super(context, menu, anchorView, overflowOnly); } @Override public void onDismiss() { super.onDismiss(); mMenu.close(); mOverflowPopup = null; } } private class ActionButtonSubmenu extends MenuPopupHelper { private SubMenuBuilder mSubMenu; public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu) { super(context, subMenu); mSubMenu = subMenu; MenuItemImpl item = (MenuItemImpl) subMenu.getItem(); if (!item.isActionButton()) { // Give a reasonable anchor to nested submenus. setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton); } } @Override public void onDismiss() { super.onDismiss(); mSubMenu.close(); mActionButtonPopup = null; } } private class OpenOverflowRunnable implements Runnable { private OverflowPopup mPopup; public OpenOverflowRunnable(OverflowPopup popup) { mPopup = popup; } public void run() { mMenu.changeMenuMode(); if (mPopup.tryShow()) { mOverflowPopup = mPopup; mPostedOpenRunnable = null; } } } }