2 * Copyright (C) 2011 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
;
19 import static com
.actionbarsherlock
.internal
.ResourcesCompat
.getResources_getInteger
;
20 import java
.util
.ArrayList
;
21 import java
.util
.HashSet
;
23 import android
.content
.Context
;
24 import android
.content
.res
.Configuration
;
25 import android
.content
.res
.Resources
;
26 import android
.os
.Build
;
27 import android
.os
.Parcel
;
28 import android
.os
.Parcelable
;
29 import android
.util
.SparseBooleanArray
;
30 import android
.view
.SoundEffectConstants
;
31 import android
.view
.View
;
32 import android
.view
.View
.MeasureSpec
;
33 import android
.view
.ViewConfiguration
;
34 import android
.view
.ViewGroup
;
35 import android
.widget
.ImageButton
;
36 import com
.actionbarsherlock
.R
;
37 import com
.actionbarsherlock
.internal
.view
.View_HasStateListenerSupport
;
38 import com
.actionbarsherlock
.internal
.view
.View_OnAttachStateChangeListener
;
39 import com
.actionbarsherlock
.internal
.view
.menu
.ActionMenuView
.ActionMenuChildView
;
40 import com
.actionbarsherlock
.view
.ActionProvider
;
41 import com
.actionbarsherlock
.view
.MenuItem
;
44 * MenuPresenter for building action menus as seen in the action bar and action modes.
46 public class ActionMenuPresenter
extends BaseMenuPresenter
47 implements ActionProvider
.SubUiVisibilityListener
{
48 //UNUSED private static final String TAG = "ActionMenuPresenter";
50 private View mOverflowButton
;
51 private boolean mReserveOverflow
;
52 private boolean mReserveOverflowSet
;
53 private int mWidthLimit
;
54 private int mActionItemWidthLimit
;
55 private int mMaxItems
;
56 private boolean mMaxItemsSet
;
57 private boolean mStrictWidthLimit
;
58 private boolean mWidthLimitSet
;
59 private boolean mExpandedActionViewsExclusive
;
61 private int mMinCellSize
;
63 // Group IDs that have been added as actions - used temporarily, allocated here for reuse.
64 private final SparseBooleanArray mActionButtonGroups
= new SparseBooleanArray();
66 private View mScrapActionButtonView
;
68 private OverflowPopup mOverflowPopup
;
69 private ActionButtonSubmenu mActionButtonPopup
;
71 private OpenOverflowRunnable mPostedOpenRunnable
;
73 final PopupPresenterCallback mPopupPresenterCallback
= new PopupPresenterCallback();
76 public ActionMenuPresenter(Context context
) {
77 super(context
, R
.layout
.abs__action_menu_layout
,
78 R
.layout
.abs__action_menu_item_layout
);
82 public void initForMenu(Context context
, MenuBuilder menu
) {
83 super.initForMenu(context
, menu
);
85 final Resources res
= context
.getResources();
87 if (!mReserveOverflowSet
) {
88 mReserveOverflow
= reserveOverflow(mContext
);
91 if (!mWidthLimitSet
) {
92 mWidthLimit
= res
.getDisplayMetrics().widthPixels
/ 2;
95 // Measure for initial configuration
97 mMaxItems
= getResources_getInteger(context
, R
.integer
.abs__max_action_buttons
);
100 int width
= mWidthLimit
;
101 if (mReserveOverflow
) {
102 if (mOverflowButton
== null
) {
103 mOverflowButton
= new OverflowMenuButton(mSystemContext
);
104 final int spec
= MeasureSpec
.makeMeasureSpec(0, MeasureSpec
.UNSPECIFIED
);
105 mOverflowButton
.measure(spec
, spec
);
107 width
-= mOverflowButton
.getMeasuredWidth();
109 mOverflowButton
= null
;
112 mActionItemWidthLimit
= width
;
114 mMinCellSize
= (int) (ActionMenuView
.MIN_CELL_SIZE
* res
.getDisplayMetrics().density
);
116 // Drop a scrap view as it may no longer reflect the proper context/config.
117 mScrapActionButtonView
= null
;
120 public static boolean reserveOverflow(Context context
) {
121 if (Build
.VERSION
.SDK_INT
< Build
.VERSION_CODES
.ICE_CREAM_SANDWICH
) {
122 return (Build
.VERSION
.SDK_INT
>= Build
.VERSION_CODES
.HONEYCOMB
);
124 return !HasPermanentMenuKey
.get(context
);
128 private static class HasPermanentMenuKey
{
129 public static boolean get(Context context
) {
130 return ViewConfiguration
.get(context
).hasPermanentMenuKey();
134 public void onConfigurationChanged(Configuration newConfig
) {
136 mMaxItems
= getResources_getInteger(mContext
,
137 R
.integer
.abs__max_action_buttons
);
139 mMenu
.onItemsChanged(true
);
144 public void setWidthLimit(int width
, boolean strict
) {
146 mStrictWidthLimit
= strict
;
147 mWidthLimitSet
= true
;
150 public void setReserveOverflow(boolean reserveOverflow
) {
151 mReserveOverflow
= reserveOverflow
;
152 mReserveOverflowSet
= true
;
155 public void setItemLimit(int itemCount
) {
156 mMaxItems
= itemCount
;
160 public void setExpandedActionViewsExclusive(boolean isExclusive
) {
161 mExpandedActionViewsExclusive
= isExclusive
;
165 public MenuView
getMenuView(ViewGroup root
) {
166 MenuView result
= super.getMenuView(root
);
167 ((ActionMenuView
) result
).setPresenter(this);
172 public View
getItemView(MenuItemImpl item
, View convertView
, ViewGroup parent
) {
173 View actionView
= item
.getActionView();
174 if (actionView
== null
|| item
.hasCollapsibleActionView()) {
175 if (!(convertView
instanceof ActionMenuItemView
)) {
178 actionView
= super.getItemView(item
, convertView
, parent
);
180 actionView
.setVisibility(item
.isActionViewExpanded() ? View
.GONE
: View
.VISIBLE
);
182 final ActionMenuView menuParent
= (ActionMenuView
) parent
;
183 final ViewGroup
.LayoutParams lp
= actionView
.getLayoutParams();
184 if (!menuParent
.checkLayoutParams(lp
)) {
185 actionView
.setLayoutParams(menuParent
.generateLayoutParams(lp
));
191 public void bindItemView(MenuItemImpl item
, MenuView
.ItemView itemView
) {
192 itemView
.initialize(item
, 0);
194 final ActionMenuView menuView
= (ActionMenuView
) mMenuView
;
195 ActionMenuItemView actionItemView
= (ActionMenuItemView
) itemView
;
196 actionItemView
.setItemInvoker(menuView
);
200 public boolean shouldIncludeItem(int childIndex
, MenuItemImpl item
) {
201 return item
.isActionButton();
205 public void updateMenuView(boolean cleared
) {
206 super.updateMenuView(cleared
);
209 final ArrayList
<MenuItemImpl
> actionItems
= mMenu
.getActionItems();
210 final int count
= actionItems
.size();
211 for (int i
= 0; i
< count
; i
++) {
212 final ActionProvider provider
= actionItems
.get(i
).getActionProvider();
213 if (provider
!= null
) {
214 provider
.setSubUiVisibilityListener(this);
219 final ArrayList
<MenuItemImpl
> nonActionItems
= mMenu
!= null ?
220 mMenu
.getNonActionItems() : null
;
222 boolean hasOverflow
= false
;
223 if (mReserveOverflow
&& nonActionItems
!= null
) {
224 final int count
= nonActionItems
.size();
226 hasOverflow
= !nonActionItems
.get(0).isActionViewExpanded();
228 hasOverflow
= count
> 0;
233 if (mOverflowButton
== null
) {
234 mOverflowButton
= new OverflowMenuButton(mSystemContext
);
236 ViewGroup parent
= (ViewGroup
) mOverflowButton
.getParent();
237 if (parent
!= mMenuView
) {
238 if (parent
!= null
) {
239 parent
.removeView(mOverflowButton
);
241 ActionMenuView menuView
= (ActionMenuView
) mMenuView
;
242 menuView
.addView(mOverflowButton
, menuView
.generateOverflowButtonLayoutParams());
244 } else if (mOverflowButton
!= null
&& mOverflowButton
.getParent() == mMenuView
) {
245 ((ViewGroup
) mMenuView
).removeView(mOverflowButton
);
248 ((ActionMenuView
) mMenuView
).setOverflowReserved(mReserveOverflow
);
252 public boolean filterLeftoverView(ViewGroup parent
, int childIndex
) {
253 if (parent
.getChildAt(childIndex
) == mOverflowButton
) return false
;
254 return super.filterLeftoverView(parent
, childIndex
);
257 public boolean onSubMenuSelected(SubMenuBuilder subMenu
) {
258 if (!subMenu
.hasVisibleItems()) return false
;
260 SubMenuBuilder topSubMenu
= subMenu
;
261 while (topSubMenu
.getParentMenu() != mMenu
) {
262 topSubMenu
= (SubMenuBuilder
) topSubMenu
.getParentMenu();
264 View anchor
= findViewForItem(topSubMenu
.getItem());
265 if (anchor
== null
) {
266 if (mOverflowButton
== null
) return false
;
267 anchor
= mOverflowButton
;
270 mOpenSubMenuId
= subMenu
.getItem().getItemId();
271 mActionButtonPopup
= new ActionButtonSubmenu(mContext
, subMenu
);
272 mActionButtonPopup
.setAnchorView(anchor
);
273 mActionButtonPopup
.show();
274 super.onSubMenuSelected(subMenu
);
278 private View
findViewForItem(MenuItem item
) {
279 final ViewGroup parent
= (ViewGroup
) mMenuView
;
280 if (parent
== null
) return null
;
282 final int count
= parent
.getChildCount();
283 for (int i
= 0; i
< count
; i
++) {
284 final View child
= parent
.getChildAt(i
);
285 if (child
instanceof MenuView
.ItemView
&&
286 ((MenuView
.ItemView
) child
).getItemData() == item
) {
294 * Display the overflow menu if one is present.
295 * @return true if the overflow menu was shown, false otherwise.
297 public boolean showOverflowMenu() {
298 if (mReserveOverflow
&& !isOverflowMenuShowing() && mMenu
!= null
&& mMenuView
!= null
&&
299 mPostedOpenRunnable
== null
&& !mMenu
.getNonActionItems().isEmpty()) {
300 OverflowPopup popup
= new OverflowPopup(mContext
, mMenu
, mOverflowButton
, true
);
301 mPostedOpenRunnable
= new OpenOverflowRunnable(popup
);
302 // Post this for later; we might still need a layout for the anchor to be right.
303 ((View
) mMenuView
).post(mPostedOpenRunnable
);
305 // ActionMenuPresenter uses null as a callback argument here
306 // to indicate overflow is opening.
307 super.onSubMenuSelected(null
);
315 * Hide the overflow menu if it is currently showing.
317 * @return true if the overflow menu was hidden, false otherwise.
319 public boolean hideOverflowMenu() {
320 if (mPostedOpenRunnable
!= null
&& mMenuView
!= null
) {
321 ((View
) mMenuView
).removeCallbacks(mPostedOpenRunnable
);
322 mPostedOpenRunnable
= null
;
326 MenuPopupHelper popup
= mOverflowPopup
;
335 * Dismiss all popup menus - overflow and submenus.
336 * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
338 public boolean dismissPopupMenus() {
339 boolean result
= hideOverflowMenu();
340 result
|= hideSubMenus();
345 * Dismiss all submenu popups.
347 * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
349 public boolean hideSubMenus() {
350 if (mActionButtonPopup
!= null
) {
351 mActionButtonPopup
.dismiss();
358 * @return true if the overflow menu is currently showing
360 public boolean isOverflowMenuShowing() {
361 return mOverflowPopup
!= null
&& mOverflowPopup
.isShowing();
365 * @return true if space has been reserved in the action menu for an overflow item.
367 public boolean isOverflowReserved() {
368 return mReserveOverflow
;
371 public boolean flagActionItems() {
372 final ArrayList
<MenuItemImpl
> visibleItems
= mMenu
.getVisibleItems();
373 final int itemsSize
= visibleItems
.size();
374 int maxActions
= mMaxItems
;
375 int widthLimit
= mActionItemWidthLimit
;
376 final int querySpec
= MeasureSpec
.makeMeasureSpec(0, MeasureSpec
.UNSPECIFIED
);
377 final ViewGroup parent
= (ViewGroup
) mMenuView
;
379 int requiredItems
= 0;
380 int requestedItems
= 0;
381 int firstActionWidth
= 0;
382 boolean hasOverflow
= false
;
383 for (int i
= 0; i
< itemsSize
; i
++) {
384 MenuItemImpl item
= visibleItems
.get(i
);
385 if (item
.requiresActionButton()) {
387 } else if (item
.requestsActionButton()) {
392 if (mExpandedActionViewsExclusive
&& item
.isActionViewExpanded()) {
393 // Overflow everything if we have an expanded action view and we're
394 // space constrained.
399 // Reserve a spot for the overflow item if needed.
400 if (mReserveOverflow
&&
401 (hasOverflow
|| requiredItems
+ requestedItems
> maxActions
)) {
404 maxActions
-= requiredItems
;
406 final SparseBooleanArray seenGroups
= mActionButtonGroups
;
410 int cellsRemaining
= 0;
411 if (mStrictWidthLimit
) {
412 cellsRemaining
= widthLimit
/ mMinCellSize
;
413 final int cellSizeRemaining
= widthLimit
% mMinCellSize
;
414 cellSize
= mMinCellSize
+ cellSizeRemaining
/ cellsRemaining
;
417 // Flag as many more requested items as will fit.
418 for (int i
= 0; i
< itemsSize
; i
++) {
419 MenuItemImpl item
= visibleItems
.get(i
);
421 if (item
.requiresActionButton()) {
422 View v
= getItemView(item
, mScrapActionButtonView
, parent
);
423 if (mScrapActionButtonView
== null
) {
424 mScrapActionButtonView
= v
;
426 if (mStrictWidthLimit
) {
427 cellsRemaining
-= ActionMenuView
.measureChildForCells(v
,
428 cellSize
, cellsRemaining
, querySpec
, 0);
430 v
.measure(querySpec
, querySpec
);
432 final int measuredWidth
= v
.getMeasuredWidth();
433 widthLimit
-= measuredWidth
;
434 if (firstActionWidth
== 0) {
435 firstActionWidth
= measuredWidth
;
437 final int groupId
= item
.getGroupId();
439 seenGroups
.put(groupId
, true
);
441 item
.setIsActionButton(true
);
442 } else if (item
.requestsActionButton()) {
443 // Items in a group with other items that already have an action slot
444 // can break the max actions rule, but not the width limit.
445 final int groupId
= item
.getGroupId();
446 final boolean inGroup
= seenGroups
.get(groupId
);
447 boolean isAction
= (maxActions
> 0 || inGroup
) && widthLimit
> 0 &&
448 (!mStrictWidthLimit
|| cellsRemaining
> 0);
451 View v
= getItemView(item
, mScrapActionButtonView
, parent
);
452 if (mScrapActionButtonView
== null
) {
453 mScrapActionButtonView
= v
;
455 if (mStrictWidthLimit
) {
456 final int cells
= ActionMenuView
.measureChildForCells(v
,
457 cellSize
, cellsRemaining
, querySpec
, 0);
458 cellsRemaining
-= cells
;
463 v
.measure(querySpec
, querySpec
);
465 final int measuredWidth
= v
.getMeasuredWidth();
466 widthLimit
-= measuredWidth
;
467 if (firstActionWidth
== 0) {
468 firstActionWidth
= measuredWidth
;
471 if (mStrictWidthLimit
) {
472 isAction
&= widthLimit
>= 0;
474 // Did this push the entire first item past the limit?
475 isAction
&= widthLimit
+ firstActionWidth
> 0;
479 if (isAction
&& groupId
!= 0) {
480 seenGroups
.put(groupId
, true
);
481 } else if (inGroup
) {
482 // We broke the width limit. Demote the whole group, they all overflow now.
483 seenGroups
.put(groupId
, false
);
484 for (int j
= 0; j
< i
; j
++) {
485 MenuItemImpl areYouMyGroupie
= visibleItems
.get(j
);
486 if (areYouMyGroupie
.getGroupId() == groupId
) {
487 // Give back the action slot
488 if (areYouMyGroupie
.isActionButton()) maxActions
++;
489 areYouMyGroupie
.setIsActionButton(false
);
494 if (isAction
) maxActions
--;
496 item
.setIsActionButton(isAction
);
503 public void onCloseMenu(MenuBuilder menu
, boolean allMenusAreClosing
) {
505 super.onCloseMenu(menu
, allMenusAreClosing
);
509 public Parcelable
onSaveInstanceState() {
510 SavedState state
= new SavedState();
511 state
.openSubMenuId
= mOpenSubMenuId
;
516 public void onRestoreInstanceState(Parcelable state
) {
517 SavedState saved
= (SavedState
) state
;
518 if (saved
.openSubMenuId
> 0) {
519 MenuItem item
= mMenu
.findItem(saved
.openSubMenuId
);
521 SubMenuBuilder subMenu
= (SubMenuBuilder
) item
.getSubMenu();
522 onSubMenuSelected(subMenu
);
528 public void onSubUiVisibilityChanged(boolean isVisible
) {
530 // Not a submenu, but treat it like one.
531 super.onSubMenuSelected(null
);
537 private static class SavedState
implements Parcelable
{
538 public int openSubMenuId
;
543 SavedState(Parcel
in) {
544 openSubMenuId
= in.readInt();
548 public int describeContents() {
553 public void writeToParcel(Parcel dest
, int flags
) {
554 dest
.writeInt(openSubMenuId
);
557 @SuppressWarnings("unused")
558 public static final Parcelable
.Creator
<SavedState
> CREATOR
559 = new Parcelable
.Creator
<SavedState
>() {
560 public SavedState
createFromParcel(Parcel
in) {
561 return new SavedState(in);
564 public SavedState
[] newArray(int size
) {
565 return new SavedState
[size
];
570 private class OverflowMenuButton
extends ImageButton
implements ActionMenuChildView
, View_HasStateListenerSupport
{
571 private final Set
<View_OnAttachStateChangeListener
> mListeners
= new HashSet
<View_OnAttachStateChangeListener
>();
573 public OverflowMenuButton(Context context
) {
574 super(context
, null
, R
.attr
.actionOverflowButtonStyle
);
578 setVisibility(VISIBLE
);
583 public boolean performClick() {
584 if (super.performClick()) {
588 playSoundEffect(SoundEffectConstants
.CLICK
);
593 public boolean needsDividerBefore() {
597 public boolean needsDividerAfter() {
602 protected void onAttachedToWindow() {
603 super.onAttachedToWindow();
604 for (View_OnAttachStateChangeListener listener
: mListeners
) {
605 listener
.onViewAttachedToWindow(this);
610 protected void onDetachedFromWindow() {
611 super.onDetachedFromWindow();
612 for (View_OnAttachStateChangeListener listener
: mListeners
) {
613 listener
.onViewDetachedFromWindow(this);
616 if (mOverflowPopup
!= null
) mOverflowPopup
.dismiss();
620 public void addOnAttachStateChangeListener(View_OnAttachStateChangeListener listener
) {
621 mListeners
.add(listener
);
625 public void removeOnAttachStateChangeListener(View_OnAttachStateChangeListener listener
) {
626 mListeners
.remove(listener
);
630 private class OverflowPopup
extends MenuPopupHelper
{
631 public OverflowPopup(Context context
, MenuBuilder menu
, View anchorView
,
632 boolean overflowOnly
) {
633 super(context
, menu
, anchorView
, overflowOnly
);
634 setCallback(mPopupPresenterCallback
);
638 public void onDismiss() {
641 mOverflowPopup
= null
;
645 private class ActionButtonSubmenu
extends MenuPopupHelper
{
646 //UNUSED private SubMenuBuilder mSubMenu;
648 public ActionButtonSubmenu(Context context
, SubMenuBuilder subMenu
) {
649 super(context
, subMenu
);
650 //UNUSED mSubMenu = subMenu;
652 MenuItemImpl item
= (MenuItemImpl
) subMenu
.getItem();
653 if (!item
.isActionButton()) {
654 // Give a reasonable anchor to nested submenus.
655 setAnchorView(mOverflowButton
== null ?
(View
) mMenuView
: mOverflowButton
);
658 setCallback(mPopupPresenterCallback
);
660 boolean preserveIconSpacing
= false
;
661 final int count
= subMenu
.size();
662 for (int i
= 0; i
< count
; i
++) {
663 MenuItem childItem
= subMenu
.getItem(i
);
664 if (childItem
.isVisible() && childItem
.getIcon() != null
) {
665 preserveIconSpacing
= true
;
669 setForceShowIcon(preserveIconSpacing
);
673 public void onDismiss() {
675 mActionButtonPopup
= null
;
680 private class PopupPresenterCallback
implements MenuPresenter
.Callback
{
683 public boolean onOpenSubMenu(MenuBuilder subMenu
) {
684 if (subMenu
== null
) return false
;
686 mOpenSubMenuId
= ((SubMenuBuilder
) subMenu
).getItem().getItemId();
691 public void onCloseMenu(MenuBuilder menu
, boolean allMenusAreClosing
) {
692 if (menu
instanceof SubMenuBuilder
) {
693 ((SubMenuBuilder
) menu
).getRootMenu().close(false
);
698 private class OpenOverflowRunnable
implements Runnable
{
699 private OverflowPopup mPopup
;
701 public OpenOverflowRunnable(OverflowPopup popup
) {
706 mMenu
.changeMenuMode();
707 final View menuView
= (View
) mMenuView
;
708 if (menuView
!= null
&& menuView
.getWindowToken() != null
&& mPopup
.tryShow()) {
709 mOverflowPopup
= mPopup
;
711 mPostedOpenRunnable
= null
;