876a22c5890fc4acf1cf1bcfefa187d337a0d0dd
[pub/Android/ownCloud.git] / actionbarsherlock / src / com / actionbarsherlock / internal / view / menu / ActionMenuPresenter.java
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
17 package com.actionbarsherlock.internal.view.menu;
18
19 import static com.actionbarsherlock.internal.ResourcesCompat.getResources_getInteger;
20 import java.util.ArrayList;
21 import java.util.HashSet;
22 import java.util.Set;
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;
42
43 /**
44 * MenuPresenter for building action menus as seen in the action bar and action modes.
45 */
46 public class ActionMenuPresenter extends BaseMenuPresenter
47 implements ActionProvider.SubUiVisibilityListener {
48 //UNUSED private static final String TAG = "ActionMenuPresenter";
49
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;
60
61 private int mMinCellSize;
62
63 // Group IDs that have been added as actions - used temporarily, allocated here for reuse.
64 private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray();
65
66 private View mScrapActionButtonView;
67
68 private OverflowPopup mOverflowPopup;
69 private ActionButtonSubmenu mActionButtonPopup;
70
71 private OpenOverflowRunnable mPostedOpenRunnable;
72
73 final PopupPresenterCallback mPopupPresenterCallback = new PopupPresenterCallback();
74 int mOpenSubMenuId;
75
76 public ActionMenuPresenter(Context context) {
77 super(context, R.layout.abs__action_menu_layout,
78 R.layout.abs__action_menu_item_layout);
79 }
80
81 @Override
82 public void initForMenu(Context context, MenuBuilder menu) {
83 super.initForMenu(context, menu);
84
85 final Resources res = context.getResources();
86
87 if (!mReserveOverflowSet) {
88 mReserveOverflow = reserveOverflow(mContext);
89 }
90
91 if (!mWidthLimitSet) {
92 mWidthLimit = res.getDisplayMetrics().widthPixels / 2;
93 }
94
95 // Measure for initial configuration
96 if (!mMaxItemsSet) {
97 mMaxItems = getResources_getInteger(context, R.integer.abs__max_action_buttons);
98 }
99
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);
106 }
107 width -= mOverflowButton.getMeasuredWidth();
108 } else {
109 mOverflowButton = null;
110 }
111
112 mActionItemWidthLimit = width;
113
114 mMinCellSize = (int) (ActionMenuView.MIN_CELL_SIZE * res.getDisplayMetrics().density);
115
116 // Drop a scrap view as it may no longer reflect the proper context/config.
117 mScrapActionButtonView = null;
118 }
119
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);
123 } else {
124 return !HasPermanentMenuKey.get(context);
125 }
126 }
127
128 private static class HasPermanentMenuKey {
129 public static boolean get(Context context) {
130 return ViewConfiguration.get(context).hasPermanentMenuKey();
131 }
132 }
133
134 public void onConfigurationChanged(Configuration newConfig) {
135 if (!mMaxItemsSet) {
136 mMaxItems = getResources_getInteger(mContext,
137 R.integer.abs__max_action_buttons);
138 if (mMenu != null) {
139 mMenu.onItemsChanged(true);
140 }
141 }
142 }
143
144 public void setWidthLimit(int width, boolean strict) {
145 mWidthLimit = width;
146 mStrictWidthLimit = strict;
147 mWidthLimitSet = true;
148 }
149
150 public void setReserveOverflow(boolean reserveOverflow) {
151 mReserveOverflow = reserveOverflow;
152 mReserveOverflowSet = true;
153 }
154
155 public void setItemLimit(int itemCount) {
156 mMaxItems = itemCount;
157 mMaxItemsSet = true;
158 }
159
160 public void setExpandedActionViewsExclusive(boolean isExclusive) {
161 mExpandedActionViewsExclusive = isExclusive;
162 }
163
164 @Override
165 public MenuView getMenuView(ViewGroup root) {
166 MenuView result = super.getMenuView(root);
167 ((ActionMenuView) result).setPresenter(this);
168 return result;
169 }
170
171 @Override
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)) {
176 convertView = null;
177 }
178 actionView = super.getItemView(item, convertView, parent);
179 }
180 actionView.setVisibility(item.isActionViewExpanded() ? View.GONE : View.VISIBLE);
181
182 final ActionMenuView menuParent = (ActionMenuView) parent;
183 final ViewGroup.LayoutParams lp = actionView.getLayoutParams();
184 if (!menuParent.checkLayoutParams(lp)) {
185 actionView.setLayoutParams(menuParent.generateLayoutParams(lp));
186 }
187 return actionView;
188 }
189
190 @Override
191 public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) {
192 itemView.initialize(item, 0);
193
194 final ActionMenuView menuView = (ActionMenuView) mMenuView;
195 ActionMenuItemView actionItemView = (ActionMenuItemView) itemView;
196 actionItemView.setItemInvoker(menuView);
197 }
198
199 @Override
200 public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) {
201 return item.isActionButton();
202 }
203
204 @Override
205 public void updateMenuView(boolean cleared) {
206 super.updateMenuView(cleared);
207
208 if (mMenu != null) {
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);
215 }
216 }
217 }
218
219 final ArrayList<MenuItemImpl> nonActionItems = mMenu != null ?
220 mMenu.getNonActionItems() : null;
221
222 boolean hasOverflow = false;
223 if (mReserveOverflow && nonActionItems != null) {
224 final int count = nonActionItems.size();
225 if (count == 1) {
226 hasOverflow = !nonActionItems.get(0).isActionViewExpanded();
227 } else {
228 hasOverflow = count > 0;
229 }
230 }
231
232 if (hasOverflow) {
233 if (mOverflowButton == null) {
234 mOverflowButton = new OverflowMenuButton(mSystemContext);
235 }
236 ViewGroup parent = (ViewGroup) mOverflowButton.getParent();
237 if (parent != mMenuView) {
238 if (parent != null) {
239 parent.removeView(mOverflowButton);
240 }
241 ActionMenuView menuView = (ActionMenuView) mMenuView;
242 menuView.addView(mOverflowButton, menuView.generateOverflowButtonLayoutParams());
243 }
244 } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) {
245 ((ViewGroup) mMenuView).removeView(mOverflowButton);
246 }
247
248 ((ActionMenuView) mMenuView).setOverflowReserved(mReserveOverflow);
249 }
250
251 @Override
252 public boolean filterLeftoverView(ViewGroup parent, int childIndex) {
253 if (parent.getChildAt(childIndex) == mOverflowButton) return false;
254 return super.filterLeftoverView(parent, childIndex);
255 }
256
257 public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
258 if (!subMenu.hasVisibleItems()) return false;
259
260 SubMenuBuilder topSubMenu = subMenu;
261 while (topSubMenu.getParentMenu() != mMenu) {
262 topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu();
263 }
264 View anchor = findViewForItem(topSubMenu.getItem());
265 if (anchor == null) {
266 if (mOverflowButton == null) return false;
267 anchor = mOverflowButton;
268 }
269
270 mOpenSubMenuId = subMenu.getItem().getItemId();
271 mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu);
272 mActionButtonPopup.setAnchorView(anchor);
273 mActionButtonPopup.show();
274 super.onSubMenuSelected(subMenu);
275 return true;
276 }
277
278 private View findViewForItem(MenuItem item) {
279 final ViewGroup parent = (ViewGroup) mMenuView;
280 if (parent == null) return null;
281
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) {
287 return child;
288 }
289 }
290 return null;
291 }
292
293 /**
294 * Display the overflow menu if one is present.
295 * @return true if the overflow menu was shown, false otherwise.
296 */
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);
304
305 // ActionMenuPresenter uses null as a callback argument here
306 // to indicate overflow is opening.
307 super.onSubMenuSelected(null);
308
309 return true;
310 }
311 return false;
312 }
313
314 /**
315 * Hide the overflow menu if it is currently showing.
316 *
317 * @return true if the overflow menu was hidden, false otherwise.
318 */
319 public boolean hideOverflowMenu() {
320 if (mPostedOpenRunnable != null && mMenuView != null) {
321 ((View) mMenuView).removeCallbacks(mPostedOpenRunnable);
322 mPostedOpenRunnable = null;
323 return true;
324 }
325
326 MenuPopupHelper popup = mOverflowPopup;
327 if (popup != null) {
328 popup.dismiss();
329 return true;
330 }
331 return false;
332 }
333
334 /**
335 * Dismiss all popup menus - overflow and submenus.
336 * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
337 */
338 public boolean dismissPopupMenus() {
339 boolean result = hideOverflowMenu();
340 result |= hideSubMenus();
341 return result;
342 }
343
344 /**
345 * Dismiss all submenu popups.
346 *
347 * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
348 */
349 public boolean hideSubMenus() {
350 if (mActionButtonPopup != null) {
351 mActionButtonPopup.dismiss();
352 return true;
353 }
354 return false;
355 }
356
357 /**
358 * @return true if the overflow menu is currently showing
359 */
360 public boolean isOverflowMenuShowing() {
361 return mOverflowPopup != null && mOverflowPopup.isShowing();
362 }
363
364 /**
365 * @return true if space has been reserved in the action menu for an overflow item.
366 */
367 public boolean isOverflowReserved() {
368 return mReserveOverflow;
369 }
370
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;
378
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()) {
386 requiredItems++;
387 } else if (item.requestsActionButton()) {
388 requestedItems++;
389 } else {
390 hasOverflow = true;
391 }
392 if (mExpandedActionViewsExclusive && item.isActionViewExpanded()) {
393 // Overflow everything if we have an expanded action view and we're
394 // space constrained.
395 maxActions = 0;
396 }
397 }
398
399 // Reserve a spot for the overflow item if needed.
400 if (mReserveOverflow &&
401 (hasOverflow || requiredItems + requestedItems > maxActions)) {
402 maxActions--;
403 }
404 maxActions -= requiredItems;
405
406 final SparseBooleanArray seenGroups = mActionButtonGroups;
407 seenGroups.clear();
408
409 int cellSize = 0;
410 int cellsRemaining = 0;
411 if (mStrictWidthLimit) {
412 cellsRemaining = widthLimit / mMinCellSize;
413 final int cellSizeRemaining = widthLimit % mMinCellSize;
414 cellSize = mMinCellSize + cellSizeRemaining / cellsRemaining;
415 }
416
417 // Flag as many more requested items as will fit.
418 for (int i = 0; i < itemsSize; i++) {
419 MenuItemImpl item = visibleItems.get(i);
420
421 if (item.requiresActionButton()) {
422 View v = getItemView(item, mScrapActionButtonView, parent);
423 if (mScrapActionButtonView == null) {
424 mScrapActionButtonView = v;
425 }
426 if (mStrictWidthLimit) {
427 cellsRemaining -= ActionMenuView.measureChildForCells(v,
428 cellSize, cellsRemaining, querySpec, 0);
429 } else {
430 v.measure(querySpec, querySpec);
431 }
432 final int measuredWidth = v.getMeasuredWidth();
433 widthLimit -= measuredWidth;
434 if (firstActionWidth == 0) {
435 firstActionWidth = measuredWidth;
436 }
437 final int groupId = item.getGroupId();
438 if (groupId != 0) {
439 seenGroups.put(groupId, true);
440 }
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);
449
450 if (isAction) {
451 View v = getItemView(item, mScrapActionButtonView, parent);
452 if (mScrapActionButtonView == null) {
453 mScrapActionButtonView = v;
454 }
455 if (mStrictWidthLimit) {
456 final int cells = ActionMenuView.measureChildForCells(v,
457 cellSize, cellsRemaining, querySpec, 0);
458 cellsRemaining -= cells;
459 if (cells == 0) {
460 isAction = false;
461 }
462 } else {
463 v.measure(querySpec, querySpec);
464 }
465 final int measuredWidth = v.getMeasuredWidth();
466 widthLimit -= measuredWidth;
467 if (firstActionWidth == 0) {
468 firstActionWidth = measuredWidth;
469 }
470
471 if (mStrictWidthLimit) {
472 isAction &= widthLimit >= 0;
473 } else {
474 // Did this push the entire first item past the limit?
475 isAction &= widthLimit + firstActionWidth > 0;
476 }
477 }
478
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);
490 }
491 }
492 }
493
494 if (isAction) maxActions--;
495
496 item.setIsActionButton(isAction);
497 }
498 }
499 return true;
500 }
501
502 @Override
503 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
504 dismissPopupMenus();
505 super.onCloseMenu(menu, allMenusAreClosing);
506 }
507
508 @Override
509 public Parcelable onSaveInstanceState() {
510 SavedState state = new SavedState();
511 state.openSubMenuId = mOpenSubMenuId;
512 return state;
513 }
514
515 @Override
516 public void onRestoreInstanceState(Parcelable state) {
517 SavedState saved = (SavedState) state;
518 if (saved.openSubMenuId > 0) {
519 MenuItem item = mMenu.findItem(saved.openSubMenuId);
520 if (item != null) {
521 SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
522 onSubMenuSelected(subMenu);
523 }
524 }
525 }
526
527 @Override
528 public void onSubUiVisibilityChanged(boolean isVisible) {
529 if (isVisible) {
530 // Not a submenu, but treat it like one.
531 super.onSubMenuSelected(null);
532 } else {
533 mMenu.close(false);
534 }
535 }
536
537 private static class SavedState implements Parcelable {
538 public int openSubMenuId;
539
540 SavedState() {
541 }
542
543 SavedState(Parcel in) {
544 openSubMenuId = in.readInt();
545 }
546
547 @Override
548 public int describeContents() {
549 return 0;
550 }
551
552 @Override
553 public void writeToParcel(Parcel dest, int flags) {
554 dest.writeInt(openSubMenuId);
555 }
556
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);
562 }
563
564 public SavedState[] newArray(int size) {
565 return new SavedState[size];
566 }
567 };
568 }
569
570 private class OverflowMenuButton extends ImageButton implements ActionMenuChildView, View_HasStateListenerSupport {
571 private final Set<View_OnAttachStateChangeListener> mListeners = new HashSet<View_OnAttachStateChangeListener>();
572
573 public OverflowMenuButton(Context context) {
574 super(context, null, R.attr.actionOverflowButtonStyle);
575
576 setClickable(true);
577 setFocusable(true);
578 setVisibility(VISIBLE);
579 setEnabled(true);
580 }
581
582 @Override
583 public boolean performClick() {
584 if (super.performClick()) {
585 return true;
586 }
587
588 playSoundEffect(SoundEffectConstants.CLICK);
589 showOverflowMenu();
590 return true;
591 }
592
593 public boolean needsDividerBefore() {
594 return false;
595 }
596
597 public boolean needsDividerAfter() {
598 return false;
599 }
600
601 @Override
602 protected void onAttachedToWindow() {
603 super.onAttachedToWindow();
604 for (View_OnAttachStateChangeListener listener : mListeners) {
605 listener.onViewAttachedToWindow(this);
606 }
607 }
608
609 @Override
610 protected void onDetachedFromWindow() {
611 super.onDetachedFromWindow();
612 for (View_OnAttachStateChangeListener listener : mListeners) {
613 listener.onViewDetachedFromWindow(this);
614 }
615
616 if (mOverflowPopup != null) mOverflowPopup.dismiss();
617 }
618
619 @Override
620 public void addOnAttachStateChangeListener(View_OnAttachStateChangeListener listener) {
621 mListeners.add(listener);
622 }
623
624 @Override
625 public void removeOnAttachStateChangeListener(View_OnAttachStateChangeListener listener) {
626 mListeners.remove(listener);
627 }
628 }
629
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);
635 }
636
637 @Override
638 public void onDismiss() {
639 super.onDismiss();
640 mMenu.close();
641 mOverflowPopup = null;
642 }
643 }
644
645 private class ActionButtonSubmenu extends MenuPopupHelper {
646 //UNUSED private SubMenuBuilder mSubMenu;
647
648 public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu) {
649 super(context, subMenu);
650 //UNUSED mSubMenu = subMenu;
651
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);
656 }
657
658 setCallback(mPopupPresenterCallback);
659
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;
666 break;
667 }
668 }
669 setForceShowIcon(preserveIconSpacing);
670 }
671
672 @Override
673 public void onDismiss() {
674 super.onDismiss();
675 mActionButtonPopup = null;
676 mOpenSubMenuId = 0;
677 }
678 }
679
680 private class PopupPresenterCallback implements MenuPresenter.Callback {
681
682 @Override
683 public boolean onOpenSubMenu(MenuBuilder subMenu) {
684 if (subMenu == null) return false;
685
686 mOpenSubMenuId = ((SubMenuBuilder) subMenu).getItem().getItemId();
687 return false;
688 }
689
690 @Override
691 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
692 if (menu instanceof SubMenuBuilder) {
693 ((SubMenuBuilder) menu).getRootMenu().close(false);
694 }
695 }
696 }
697
698 private class OpenOverflowRunnable implements Runnable {
699 private OverflowPopup mPopup;
700
701 public OpenOverflowRunnable(OverflowPopup popup) {
702 mPopup = popup;
703 }
704
705 public void run() {
706 mMenu.changeMenuMode();
707 final View menuView = (View) mMenuView;
708 if (menuView != null && menuView.getWindowToken() != null && mPopup.tryShow()) {
709 mOverflowPopup = mPopup;
710 }
711 mPostedOpenRunnable = null;
712 }
713 }
714 }