1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v7.internal.view.menu;
18
19import android.content.Context;
20import android.content.DialogInterface;
21import android.content.res.Configuration;
22import android.content.res.Resources;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.support.v7.appcompat.R;
26import android.support.v7.internal.view.ActionBarPolicy;
27import android.support.v4.view.ActionProvider;
28import android.support.v7.internal.view.menu.ActionMenuView.ActionMenuChildView;
29import android.util.SparseBooleanArray;
30import android.view.MenuItem;
31import android.view.SoundEffectConstants;
32import android.view.View;
33import android.view.View.MeasureSpec;
34import android.view.ViewGroup;
35import android.widget.ImageButton;
36
37import java.util.ArrayList;
38
39/**
40 * MenuPresenter for building action menus as seen in the action bar and action modes.
41 *
42 * @hide
43 */
44public class ActionMenuPresenter extends BaseMenuPresenter
45        implements ActionProvider.SubUiVisibilityListener {
46
47    private static final String TAG = "ActionMenuPresenter";
48
49    private View mOverflowButton;
50    private boolean mReserveOverflow;
51    private boolean mReserveOverflowSet;
52    private int mWidthLimit;
53    private int mActionItemWidthLimit;
54    private int mMaxItems;
55    private boolean mMaxItemsSet;
56    private boolean mStrictWidthLimit;
57    private boolean mWidthLimitSet;
58    private boolean mExpandedActionViewsExclusive;
59
60    private int mMinCellSize;
61
62    // Group IDs that have been added as actions - used temporarily, allocated here for reuse.
63    private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray();
64
65    private View mScrapActionButtonView;
66
67    private OverflowPopup mOverflowPopup;
68    private ActionButtonSubmenu mActionButtonPopup;
69
70    private OpenOverflowRunnable mPostedOpenRunnable;
71
72    final PopupPresenterCallback mPopupPresenterCallback = new PopupPresenterCallback();
73    int mOpenSubMenuId;
74
75    public ActionMenuPresenter(Context context) {
76        super(context, R.layout.abc_action_menu_layout, R.layout.abc_action_menu_item_layout);
77    }
78
79    @Override
80    public void initForMenu(Context context, MenuBuilder menu) {
81        super.initForMenu(context, menu);
82
83        final Resources res = context.getResources();
84
85        final ActionBarPolicy abp = ActionBarPolicy.get(context);
86        if (!mReserveOverflowSet) {
87            mReserveOverflow = abp.showsOverflowMenuButton();
88        }
89
90        if (!mWidthLimitSet) {
91            mWidthLimit = abp.getEmbeddedMenuWidthLimit();
92        }
93
94        // Measure for initial configuration
95        if (!mMaxItemsSet) {
96            mMaxItems = abp.getMaxActionButtons();
97        }
98
99        int width = mWidthLimit;
100        if (mReserveOverflow) {
101            if (mOverflowButton == null) {
102                mOverflowButton = new OverflowMenuButton(mSystemContext);
103                final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
104                mOverflowButton.measure(spec, spec);
105            }
106            width -= mOverflowButton.getMeasuredWidth();
107        } else {
108            mOverflowButton = null;
109        }
110
111        mActionItemWidthLimit = width;
112
113        mMinCellSize = (int) (ActionMenuView.MIN_CELL_SIZE * res.getDisplayMetrics().density);
114
115        // Drop a scrap view as it may no longer reflect the proper context/config.
116        mScrapActionButtonView = null;
117    }
118
119    public void onConfigurationChanged(Configuration newConfig) {
120        if (!mMaxItemsSet) {
121            mMaxItems = mContext.getResources().getInteger(
122                    R.integer.abc_max_action_buttons);
123        }
124        if (mMenu != null) {
125            mMenu.onItemsChanged(true);
126        }
127    }
128
129    public void setWidthLimit(int width, boolean strict) {
130        mWidthLimit = width;
131        mStrictWidthLimit = strict;
132        mWidthLimitSet = true;
133    }
134
135    public void setReserveOverflow(boolean reserveOverflow) {
136        mReserveOverflow = reserveOverflow;
137        mReserveOverflowSet = true;
138    }
139
140    public void setItemLimit(int itemCount) {
141        mMaxItems = itemCount;
142        mMaxItemsSet = true;
143    }
144
145    public void setExpandedActionViewsExclusive(boolean isExclusive) {
146        mExpandedActionViewsExclusive = isExclusive;
147    }
148
149    @Override
150    public MenuView getMenuView(ViewGroup root) {
151        MenuView result = super.getMenuView(root);
152        ((ActionMenuView) result).setPresenter(this);
153        return result;
154    }
155
156    @Override
157    public View getItemView(MenuItemImpl item, View convertView, ViewGroup parent) {
158        View actionView = item.getActionView();
159        if (actionView == null || item.hasCollapsibleActionView()) {
160            if (!(convertView instanceof ActionMenuItemView)) {
161                convertView = null;
162            }
163            actionView = super.getItemView(item, convertView, parent);
164        }
165        actionView.setVisibility(item.isActionViewExpanded() ? View.GONE : View.VISIBLE);
166
167        final ActionMenuView menuParent = (ActionMenuView) parent;
168        final ViewGroup.LayoutParams lp = actionView.getLayoutParams();
169        if (!menuParent.checkLayoutParams(lp)) {
170            actionView.setLayoutParams(menuParent.generateLayoutParams(lp));
171        }
172        return actionView;
173    }
174
175    @Override
176    public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) {
177        itemView.initialize(item, 0);
178
179        final ActionMenuView menuView = (ActionMenuView) mMenuView;
180        ActionMenuItemView actionItemView = (ActionMenuItemView) itemView;
181        actionItemView.setItemInvoker(menuView);
182    }
183
184    @Override
185    public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) {
186        return item.isActionButton();
187    }
188
189    @Override
190    public void updateMenuView(boolean cleared) {
191        super.updateMenuView(cleared);
192
193        if (mMenuView == null) {
194            return;
195        }
196
197        if (mMenu != null) {
198            final ArrayList<MenuItemImpl> actionItems = mMenu.getActionItems();
199            final int count = actionItems.size();
200            for (int i = 0; i < count; i++) {
201                final ActionProvider provider = actionItems.get(i).getSupportActionProvider();
202                if (provider != null) {
203                    provider.setSubUiVisibilityListener(this);
204                }
205            }
206        }
207
208        final ArrayList<MenuItemImpl> nonActionItems = mMenu != null ?
209                mMenu.getNonActionItems() : null;
210
211        boolean hasOverflow = false;
212        if (mReserveOverflow && nonActionItems != null) {
213            final int count = nonActionItems.size();
214            if (count == 1) {
215                hasOverflow = !nonActionItems.get(0).isActionViewExpanded();
216            } else {
217                hasOverflow = count > 0;
218            }
219        }
220
221        if (hasOverflow) {
222            if (mOverflowButton == null) {
223                mOverflowButton = new OverflowMenuButton(mSystemContext);
224            }
225            ViewGroup parent = (ViewGroup) mOverflowButton.getParent();
226            if (parent != mMenuView) {
227                if (parent != null) {
228                    parent.removeView(mOverflowButton);
229                }
230                ActionMenuView menuView = (ActionMenuView) mMenuView;
231                menuView.addView(mOverflowButton, menuView.generateOverflowButtonLayoutParams());
232            }
233        } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) {
234            ((ViewGroup) mMenuView).removeView(mOverflowButton);
235        }
236
237        ((ActionMenuView) mMenuView).setOverflowReserved(mReserveOverflow);
238    }
239
240    @Override
241    public boolean filterLeftoverView(ViewGroup parent, int childIndex) {
242        if (parent.getChildAt(childIndex) == mOverflowButton) {
243            return false;
244        }
245        return super.filterLeftoverView(parent, childIndex);
246    }
247
248    public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
249        if (!subMenu.hasVisibleItems()) {
250            return false;
251        }
252
253        SubMenuBuilder topSubMenu = subMenu;
254        while (topSubMenu.getParentMenu() != mMenu) {
255            topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu();
256        }
257        View anchor = findViewForItem(topSubMenu.getItem());
258        if (anchor == null) {
259            if (mOverflowButton == null) {
260                return false;
261            }
262            anchor = mOverflowButton;
263        }
264
265        mOpenSubMenuId = subMenu.getItem().getItemId();
266        mActionButtonPopup = new ActionButtonSubmenu(subMenu);
267        mActionButtonPopup.show(null);
268        super.onSubMenuSelected(subMenu);
269        return true;
270    }
271
272    private View findViewForItem(MenuItem item) {
273        final ViewGroup parent = (ViewGroup) mMenuView;
274        if (parent == null) {
275            return null;
276        }
277
278        final int count = parent.getChildCount();
279        for (int i = 0; i < count; i++) {
280            final View child = parent.getChildAt(i);
281            if (child instanceof MenuView.ItemView &&
282                    ((MenuView.ItemView) child).getItemData() == item) {
283                return child;
284            }
285        }
286        return null;
287    }
288
289    /**
290     * Display the overflow menu if one is present.
291     *
292     * @return true if the overflow menu was shown, false otherwise.
293     */
294    public boolean showOverflowMenu() {
295        if (mReserveOverflow && !isOverflowMenuShowing() && mMenu != null && mMenuView != null &&
296                mPostedOpenRunnable == null) {
297            OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true);
298            mPostedOpenRunnable = new OpenOverflowRunnable(popup);
299            // Post this for later; we might still need a layout for the anchor to be right.
300            ((View) mMenuView).post(mPostedOpenRunnable);
301
302            // ActionMenuPresenter uses null as a callback argument here
303            // to indicate overflow is opening.
304            super.onSubMenuSelected(null);
305
306            return true;
307        }
308        return false;
309    }
310
311    /**
312     * Hide the overflow menu if it is currently showing.
313     *
314     * @return true if the overflow menu was hidden, false otherwise.
315     */
316    public boolean hideOverflowMenu() {
317        if (mPostedOpenRunnable != null && mMenuView != null) {
318            ((View) mMenuView).removeCallbacks(mPostedOpenRunnable);
319            mPostedOpenRunnable = null;
320            return true;
321        }
322
323        MenuPopupHelper popup = mOverflowPopup;
324        if (popup != null) {
325            popup.dismiss();
326            return true;
327        }
328        return false;
329    }
330
331    /**
332     * Dismiss all popup menus - overflow and submenus.
333     *
334     * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
335     */
336    public boolean dismissPopupMenus() {
337        boolean result = hideOverflowMenu();
338        result |= hideSubMenus();
339        return result;
340    }
341
342    /**
343     * Dismiss all submenu popups.
344     *
345     * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
346     */
347    public boolean hideSubMenus() {
348        if (mActionButtonPopup != null) {
349            mActionButtonPopup.dismiss();
350            return true;
351        }
352        return false;
353    }
354
355    /**
356     * @return true if the overflow menu is currently showing
357     */
358    public boolean isOverflowMenuShowing() {
359        return mOverflowPopup != null && mOverflowPopup.isShowing();
360    }
361
362    /**
363     * @return true if space has been reserved in the action menu for an overflow item.
364     */
365    public boolean isOverflowReserved() {
366        return mReserveOverflow;
367    }
368
369    public boolean flagActionItems() {
370        final ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems();
371        final int itemsSize = visibleItems.size();
372        int maxActions = mMaxItems;
373        int widthLimit = mActionItemWidthLimit;
374        final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
375        final ViewGroup parent = (ViewGroup) mMenuView;
376
377        int requiredItems = 0;
378        int requestedItems = 0;
379        int firstActionWidth = 0;
380        boolean hasOverflow = false;
381        for (int i = 0; i < itemsSize; i++) {
382            MenuItemImpl item = visibleItems.get(i);
383            if (item.requiresActionButton()) {
384                requiredItems++;
385            } else if (item.requestsActionButton()) {
386                requestedItems++;
387            } else {
388                hasOverflow = true;
389            }
390            if (mExpandedActionViewsExclusive && item.isActionViewExpanded()) {
391                // Overflow everything if we have an expanded action view and we're
392                // space constrained.
393                maxActions = 0;
394            }
395        }
396
397        // Reserve a spot for the overflow item if needed.
398        if (mReserveOverflow &&
399                (hasOverflow || requiredItems + requestedItems > maxActions)) {
400            maxActions--;
401        }
402        maxActions -= requiredItems;
403
404        final SparseBooleanArray seenGroups = mActionButtonGroups;
405        seenGroups.clear();
406
407        int cellSize = 0;
408        int cellsRemaining = 0;
409        if (mStrictWidthLimit) {
410            cellsRemaining = widthLimit / mMinCellSize;
411            final int cellSizeRemaining = widthLimit % mMinCellSize;
412            cellSize = mMinCellSize + cellSizeRemaining / cellsRemaining;
413        }
414
415        // Flag as many more requested items as will fit.
416        for (int i = 0; i < itemsSize; i++) {
417            MenuItemImpl item = visibleItems.get(i);
418
419            if (item.requiresActionButton()) {
420                View v = getItemView(item, mScrapActionButtonView, parent);
421                if (mScrapActionButtonView == null) {
422                    mScrapActionButtonView = v;
423                }
424                if (mStrictWidthLimit) {
425                    cellsRemaining -= ActionMenuView.measureChildForCells(v,
426                            cellSize, cellsRemaining, querySpec, 0);
427                } else {
428                    v.measure(querySpec, querySpec);
429                }
430                final int measuredWidth = v.getMeasuredWidth();
431                widthLimit -= measuredWidth;
432                if (firstActionWidth == 0) {
433                    firstActionWidth = measuredWidth;
434                }
435                final int groupId = item.getGroupId();
436                if (groupId != 0) {
437                    seenGroups.put(groupId, true);
438                }
439                item.setIsActionButton(true);
440            } else if (item.requestsActionButton()) {
441                // Items in a group with other items that already have an action slot
442                // can break the max actions rule, but not the width limit.
443                final int groupId = item.getGroupId();
444                final boolean inGroup = seenGroups.get(groupId);
445                boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0 &&
446                        (!mStrictWidthLimit || cellsRemaining > 0);
447
448                if (isAction) {
449                    View v = getItemView(item, mScrapActionButtonView, parent);
450                    if (mScrapActionButtonView == null) {
451                        mScrapActionButtonView = v;
452                    }
453                    if (mStrictWidthLimit) {
454                        final int cells = ActionMenuView.measureChildForCells(v,
455                                cellSize, cellsRemaining, querySpec, 0);
456                        cellsRemaining -= cells;
457                        if (cells == 0) {
458                            isAction = false;
459                        }
460                    } else {
461                        v.measure(querySpec, querySpec);
462                    }
463                    final int measuredWidth = v.getMeasuredWidth();
464                    widthLimit -= measuredWidth;
465                    if (firstActionWidth == 0) {
466                        firstActionWidth = measuredWidth;
467                    }
468
469                    if (mStrictWidthLimit) {
470                        isAction &= widthLimit >= 0;
471                    } else {
472                        // Did this push the entire first item past the limit?
473                        isAction &= widthLimit + firstActionWidth > 0;
474                    }
475                }
476
477                if (isAction && groupId != 0) {
478                    seenGroups.put(groupId, true);
479                } else if (inGroup) {
480                    // We broke the width limit. Demote the whole group, they all overflow now.
481                    seenGroups.put(groupId, false);
482                    for (int j = 0; j < i; j++) {
483                        MenuItemImpl areYouMyGroupie = visibleItems.get(j);
484                        if (areYouMyGroupie.getGroupId() == groupId) {
485                            // Give back the action slot
486                            if (areYouMyGroupie.isActionButton()) {
487                                maxActions++;
488                            }
489                            areYouMyGroupie.setIsActionButton(false);
490                        }
491                    }
492                }
493
494                if (isAction) {
495                    maxActions--;
496                }
497
498                item.setIsActionButton(isAction);
499            }
500        }
501        return true;
502    }
503
504    @Override
505    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
506        dismissPopupMenus();
507        super.onCloseMenu(menu, allMenusAreClosing);
508    }
509
510    @Override
511    public Parcelable onSaveInstanceState() {
512        SavedState state = new SavedState();
513        state.openSubMenuId = mOpenSubMenuId;
514        return state;
515    }
516
517    @Override
518    public void onRestoreInstanceState(Parcelable state) {
519        SavedState saved = (SavedState) state;
520        if (saved.openSubMenuId > 0) {
521            MenuItem item = mMenu.findItem(saved.openSubMenuId);
522            if (item != null) {
523                SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
524                onSubMenuSelected(subMenu);
525            }
526        }
527    }
528
529    @Override
530    public void onSubUiVisibilityChanged(boolean isVisible) {
531        if (isVisible) {
532            // Not a submenu, but treat it like one.
533            super.onSubMenuSelected(null);
534        } else {
535            mMenu.close(false);
536        }
537    }
538
539    private static class SavedState implements Parcelable {
540
541        public int openSubMenuId;
542
543        SavedState() {
544        }
545
546        SavedState(Parcel in) {
547            openSubMenuId = in.readInt();
548        }
549
550        @Override
551        public int describeContents() {
552            return 0;
553        }
554
555        @Override
556        public void writeToParcel(Parcel dest, int flags) {
557            dest.writeInt(openSubMenuId);
558        }
559
560        public static final Parcelable.Creator<SavedState> CREATOR
561                = new Parcelable.Creator<SavedState>() {
562            public SavedState createFromParcel(Parcel in) {
563                return new SavedState(in);
564            }
565
566            public SavedState[] newArray(int size) {
567                return new SavedState[size];
568            }
569        };
570    }
571
572    private class OverflowMenuButton extends ImageButton implements ActionMenuChildView {
573
574        public OverflowMenuButton(Context context) {
575            super(context, null, R.attr.actionOverflowButtonStyle);
576
577            setClickable(true);
578            setFocusable(true);
579            setVisibility(VISIBLE);
580            setEnabled(true);
581        }
582
583        @Override
584        public boolean performClick() {
585            if (super.performClick()) {
586                return true;
587            }
588
589            playSoundEffect(SoundEffectConstants.CLICK);
590            showOverflowMenu();
591            return true;
592        }
593
594        public boolean needsDividerBefore() {
595            return false;
596        }
597
598        public boolean needsDividerAfter() {
599            return false;
600        }
601    }
602
603    private class OverflowPopup extends MenuPopupHelper {
604
605        public OverflowPopup(Context context, MenuBuilder menu, View anchorView,
606                boolean overflowOnly) {
607            super(context, menu, anchorView, overflowOnly);
608            setCallback(mPopupPresenterCallback);
609        }
610
611        @Override
612        public void onDismiss() {
613            super.onDismiss();
614            mMenu.close();
615            mOverflowPopup = null;
616        }
617    }
618
619    private class ActionButtonSubmenu extends MenuDialogHelper {
620
621        public ActionButtonSubmenu(SubMenuBuilder subMenu) {
622            super(subMenu);
623            setCallback(mPopupPresenterCallback);
624        }
625
626        @Override
627        public void onDismiss(DialogInterface dialog) {
628            super.onDismiss(dialog);
629            mActionButtonPopup = null;
630            mOpenSubMenuId = 0;
631        }
632    }
633
634    private class PopupPresenterCallback implements MenuPresenter.Callback {
635
636        @Override
637        public boolean onOpenSubMenu(MenuBuilder subMenu) {
638            if (subMenu == null) {
639                return false;
640            }
641
642            mOpenSubMenuId = ((SubMenuBuilder) subMenu).getItem().getItemId();
643            return false;
644        }
645
646        @Override
647        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
648            if (menu instanceof SubMenuBuilder) {
649                ((SubMenuBuilder) menu).getRootMenu().close(false);
650            }
651        }
652    }
653
654    private class OpenOverflowRunnable implements Runnable {
655
656        private OverflowPopup mPopup;
657
658        public OpenOverflowRunnable(OverflowPopup popup) {
659            mPopup = popup;
660        }
661
662        public void run() {
663            mMenu.changeMenuMode();
664            final View menuView = (View) mMenuView;
665            if (menuView != null && menuView.getWindowToken() != null && mPopup.tryShow()) {
666                mOverflowPopup = mPopup;
667            }
668            mPostedOpenRunnable = null;
669        }
670    }
671}
672