blob: 91dd7e36f7ba359f7098456eb4140e91a31da4d5 [file] [log] [blame]
Adam Powell696cba52011-03-29 10:38:16 -07001/*
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 com.android.internal.view.menu;
18
19import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView;
20
21import android.content.Context;
22import android.content.res.Configuration;
23import android.content.res.Resources;
Adam Powell696cba52011-03-29 10:38:16 -070024import android.util.SparseBooleanArray;
25import android.view.MenuItem;
26import android.view.SoundEffectConstants;
27import android.view.View;
28import android.view.View.MeasureSpec;
29import android.view.ViewGroup;
30import android.widget.ImageButton;
31
32import java.util.ArrayList;
33
34/**
35 * MenuPresenter for building action menus as seen in the action bar and action modes.
36 */
37public class ActionMenuPresenter extends BaseMenuPresenter {
Adam Powell640a66e2011-04-29 10:18:53 -070038 private static final String TAG = "ActionMenuPresenter";
39
Adam Powell696cba52011-03-29 10:38:16 -070040 private View mOverflowButton;
41 private boolean mReserveOverflow;
42 private int mWidthLimit;
43 private int mActionItemWidthLimit;
44 private int mMaxItems;
Adam Powell640a66e2011-04-29 10:18:53 -070045 private boolean mStrictWidthLimit;
Adam Powell696cba52011-03-29 10:38:16 -070046
47 // Group IDs that have been added as actions - used temporarily, allocated here for reuse.
48 private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray();
49
50 private View mScrapActionButtonView;
51
52 private OverflowPopup mOverflowPopup;
53 private ActionButtonSubmenu mActionButtonPopup;
54
55 private OpenOverflowRunnable mPostedOpenRunnable;
56
57 public ActionMenuPresenter() {
58 super(com.android.internal.R.layout.action_menu_layout,
59 com.android.internal.R.layout.action_menu_item_layout);
60 }
61
62 @Override
63 public void initForMenu(Context context, MenuBuilder menu) {
64 super.initForMenu(context, menu);
65
66 final Resources res = context.getResources();
67 final int screen = res.getConfiguration().screenLayout;
68 // TODO Use the no-buttons specifier instead here
69 mReserveOverflow = (screen & Configuration.SCREENLAYOUT_SIZE_MASK) ==
70 Configuration.SCREENLAYOUT_SIZE_XLARGE;
71 mWidthLimit = res.getDisplayMetrics().widthPixels / 2;
72
73 // Measure for initial configuration
74 mMaxItems = res.getInteger(com.android.internal.R.integer.max_action_buttons);
75
76 int width = mWidthLimit;
77 if (mReserveOverflow) {
Adam Powell9b4bee02011-04-27 19:24:47 -070078 if (mOverflowButton == null) {
79 mOverflowButton = new OverflowMenuButton(mContext);
80 final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
81 mOverflowButton.measure(spec, spec);
82 }
Adam Powell696cba52011-03-29 10:38:16 -070083 width -= mOverflowButton.getMeasuredWidth();
84 } else {
85 mOverflowButton = null;
86 }
87
88 mActionItemWidthLimit = width;
89
90 // Drop a scrap view as it may no longer reflect the proper context/config.
91 mScrapActionButtonView = null;
92 }
93
Adam Powell640a66e2011-04-29 10:18:53 -070094 public void setWidthLimit(int width, boolean strict) {
Adam Powell9b4bee02011-04-27 19:24:47 -070095 if (mReserveOverflow) {
96 width -= mOverflowButton.getMeasuredWidth();
97 }
98 mActionItemWidthLimit = width;
Adam Powell640a66e2011-04-29 10:18:53 -070099 mStrictWidthLimit = strict;
Adam Powell9b4bee02011-04-27 19:24:47 -0700100 }
101
102 public void setItemLimit(int itemCount) {
103 mMaxItems = itemCount;
104 }
105
Adam Powell696cba52011-03-29 10:38:16 -0700106 @Override
107 public MenuView getMenuView(ViewGroup root) {
108 MenuView result = super.getMenuView(root);
109 ((ActionMenuView) result).setPresenter(this);
110 return result;
111 }
112
113 @Override
114 public View getItemView(MenuItemImpl item, View convertView, ViewGroup parent) {
Adam Powell8d02dea2011-05-31 21:35:13 -0700115 View actionView = item.getActionView();
116 actionView = actionView != null && !item.hasCollapsibleActionView() ?
117 actionView : super.getItemView(item, convertView, parent);
118 actionView.setVisibility(item.isActionViewExpanded() ? View.GONE : View.VISIBLE);
119 return actionView;
Adam Powell696cba52011-03-29 10:38:16 -0700120 }
121
122 @Override
123 public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) {
124 itemView.initialize(item, 0);
125 ((ActionMenuItemView) itemView).setItemInvoker((ActionMenuView) mMenuView);
126 }
127
128 @Override
129 public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) {
130 return item.isActionButton();
131 }
132
133 @Override
134 public void updateMenuView(boolean cleared) {
135 super.updateMenuView(cleared);
136
137 if (mReserveOverflow && mMenu.getNonActionItems().size() > 0) {
138 if (mOverflowButton == null) {
139 mOverflowButton = new OverflowMenuButton(mContext);
Adam Powell640a66e2011-04-29 10:18:53 -0700140 mOverflowButton.setLayoutParams(
141 ((ActionMenuView) mMenuView).generateOverflowButtonLayoutParams());
Adam Powell696cba52011-03-29 10:38:16 -0700142 }
143 ViewGroup parent = (ViewGroup) mOverflowButton.getParent();
144 if (parent != mMenuView) {
145 if (parent != null) {
146 parent.removeView(mOverflowButton);
147 }
148 ((ViewGroup) mMenuView).addView(mOverflowButton);
149 }
150 } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) {
151 ((ViewGroup) mMenuView).removeView(mOverflowButton);
152 }
153 }
154
155 @Override
156 public boolean filterLeftoverView(ViewGroup parent, int childIndex) {
157 if (parent.getChildAt(childIndex) == mOverflowButton) return false;
158 return super.filterLeftoverView(parent, childIndex);
159 }
160
161 public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
162 if (!subMenu.hasVisibleItems()) return false;
163
164 SubMenuBuilder topSubMenu = subMenu;
165 while (topSubMenu.getParentMenu() != mMenu) {
166 topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu();
167 }
168 View anchor = findViewForItem(topSubMenu.getItem());
169 if (anchor == null) return false;
170
171 mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu);
172 mActionButtonPopup.setAnchorView(anchor);
173 mActionButtonPopup.show();
174 super.onSubMenuSelected(subMenu);
175 return true;
176 }
177
178 private View findViewForItem(MenuItem item) {
179 final ViewGroup parent = (ViewGroup) mMenuView;
180 if (parent == null) return null;
181
182 final int count = parent.getChildCount();
183 for (int i = 0; i < count; i++) {
184 final View child = parent.getChildAt(i);
185 if (child instanceof MenuView.ItemView &&
186 ((MenuView.ItemView) child).getItemData() == item) {
187 return child;
188 }
189 }
190 return null;
191 }
192
193 /**
194 * Display the overflow menu if one is present.
195 * @return true if the overflow menu was shown, false otherwise.
196 */
197 public boolean showOverflowMenu() {
198 if (mReserveOverflow && !isOverflowMenuShowing() && mMenuView != null &&
199 mPostedOpenRunnable == null) {
Adam Powell696cba52011-03-29 10:38:16 -0700200 OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true);
201 mPostedOpenRunnable = new OpenOverflowRunnable(popup);
202 // Post this for later; we might still need a layout for the anchor to be right.
203 ((View) mMenuView).post(mPostedOpenRunnable);
204
205 // ActionMenuPresenter uses null as a callback argument here
206 // to indicate overflow is opening.
207 super.onSubMenuSelected(null);
208
209 return true;
210 }
211 return false;
212 }
213
214 /**
215 * Hide the overflow menu if it is currently showing.
216 *
217 * @return true if the overflow menu was hidden, false otherwise.
218 */
219 public boolean hideOverflowMenu() {
220 if (mPostedOpenRunnable != null && mMenuView != null) {
221 ((View) mMenuView).removeCallbacks(mPostedOpenRunnable);
222 return true;
223 }
224
225 MenuPopupHelper popup = mOverflowPopup;
226 if (popup != null) {
227 popup.dismiss();
228 return true;
229 }
230 return false;
231 }
232
233 /**
234 * Dismiss all popup menus - overflow and submenus.
235 * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
236 */
237 public boolean dismissPopupMenus() {
238 boolean result = hideOverflowMenu();
239 result |= hideSubMenus();
240 return result;
241 }
242
243 /**
244 * Dismiss all submenu popups.
245 *
246 * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
247 */
248 public boolean hideSubMenus() {
249 if (mActionButtonPopup != null) {
250 mActionButtonPopup.dismiss();
251 return true;
252 }
253 return false;
254 }
255
256 /**
257 * @return true if the overflow menu is currently showing
258 */
259 public boolean isOverflowMenuShowing() {
260 return mOverflowPopup != null && mOverflowPopup.isShowing();
261 }
262
263 /**
264 * @return true if space has been reserved in the action menu for an overflow item.
265 */
266 public boolean isOverflowReserved() {
267 return mReserveOverflow;
268 }
269
270 public boolean flagActionItems() {
271 final ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems();
272 final int itemsSize = visibleItems.size();
273 int maxActions = mMaxItems;
274 int widthLimit = mActionItemWidthLimit;
275 final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
276 final ViewGroup parent = (ViewGroup) mMenuView;
277
278 int requiredItems = 0;
279 int requestedItems = 0;
280 int firstActionWidth = 0;
281 boolean hasOverflow = false;
282 for (int i = 0; i < itemsSize; i++) {
283 MenuItemImpl item = visibleItems.get(i);
284 if (item.requiresActionButton()) {
285 requiredItems++;
286 } else if (item.requestsActionButton()) {
287 requestedItems++;
288 } else {
289 hasOverflow = true;
290 }
291 }
292
293 // Reserve a spot for the overflow item if needed.
294 if (mReserveOverflow &&
295 (hasOverflow || requiredItems + requestedItems > maxActions)) {
296 maxActions--;
297 }
298 maxActions -= requiredItems;
299
300 final SparseBooleanArray seenGroups = mActionButtonGroups;
301 seenGroups.clear();
302
303 // Flag as many more requested items as will fit.
304 for (int i = 0; i < itemsSize; i++) {
305 MenuItemImpl item = visibleItems.get(i);
306
307 if (item.requiresActionButton()) {
308 View v = item.getActionView();
Adam Powell8d02dea2011-05-31 21:35:13 -0700309 if (v == null || item.hasCollapsibleActionView()) {
Adam Powell696cba52011-03-29 10:38:16 -0700310 v = getItemView(item, mScrapActionButtonView, parent);
311 if (mScrapActionButtonView == null) {
312 mScrapActionButtonView = v;
313 }
314 }
315 v.measure(querySpec, querySpec);
316 final int measuredWidth = v.getMeasuredWidth();
317 widthLimit -= measuredWidth;
318 if (firstActionWidth == 0) {
319 firstActionWidth = measuredWidth;
320 }
321 final int groupId = item.getGroupId();
322 if (groupId != 0) {
323 seenGroups.put(groupId, true);
324 }
325 } else if (item.requestsActionButton()) {
326 // Items in a group with other items that already have an action slot
327 // can break the max actions rule, but not the width limit.
328 final int groupId = item.getGroupId();
329 final boolean inGroup = seenGroups.get(groupId);
330 boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0;
331 maxActions--;
332
333 if (isAction) {
334 View v = item.getActionView();
Adam Powell8d02dea2011-05-31 21:35:13 -0700335 if (v == null || item.hasCollapsibleActionView()) {
Adam Powell696cba52011-03-29 10:38:16 -0700336 v = getItemView(item, mScrapActionButtonView, parent);
337 if (mScrapActionButtonView == null) {
338 mScrapActionButtonView = v;
339 }
340 }
341 v.measure(querySpec, querySpec);
342 final int measuredWidth = v.getMeasuredWidth();
343 widthLimit -= measuredWidth;
344 if (firstActionWidth == 0) {
345 firstActionWidth = measuredWidth;
346 }
347
Adam Powell640a66e2011-04-29 10:18:53 -0700348 if (mStrictWidthLimit) {
349 isAction = widthLimit >= 0;
Adam Powell640a66e2011-04-29 10:18:53 -0700350 } else {
351 // Did this push the entire first item past the limit?
352 isAction = widthLimit + firstActionWidth > 0;
Adam Powell696cba52011-03-29 10:38:16 -0700353 }
354 }
355
356 if (isAction && groupId != 0) {
357 seenGroups.put(groupId, true);
358 } else if (inGroup) {
359 // We broke the width limit. Demote the whole group, they all overflow now.
360 seenGroups.put(groupId, false);
361 for (int j = 0; j < i; j++) {
362 MenuItemImpl areYouMyGroupie = visibleItems.get(j);
363 if (areYouMyGroupie.getGroupId() == groupId) {
364 areYouMyGroupie.setIsActionButton(false);
365 }
366 }
367 }
368
369 item.setIsActionButton(isAction);
370 }
371 }
372 return true;
373 }
374
375 @Override
376 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
377 dismissPopupMenus();
378 super.onCloseMenu(menu, allMenusAreClosing);
379 }
380
381 private class OverflowMenuButton extends ImageButton implements ActionMenuChildView {
382 public OverflowMenuButton(Context context) {
383 super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle);
384
385 setClickable(true);
386 setFocusable(true);
387 setVisibility(VISIBLE);
388 setEnabled(true);
389 }
390
391 @Override
392 public boolean performClick() {
393 if (super.performClick()) {
394 return true;
395 }
396
397 playSoundEffect(SoundEffectConstants.CLICK);
398 showOverflowMenu();
399 return true;
400 }
401
402 public boolean needsDividerBefore() {
403 return true;
404 }
405
406 public boolean needsDividerAfter() {
407 return false;
408 }
409 }
410
411 private class OverflowPopup extends MenuPopupHelper {
412 public OverflowPopup(Context context, MenuBuilder menu, View anchorView,
413 boolean overflowOnly) {
414 super(context, menu, anchorView, overflowOnly);
415 }
416
417 @Override
418 public void onDismiss() {
419 super.onDismiss();
420 mMenu.close();
421 mOverflowPopup = null;
422 }
423 }
424
425 private class ActionButtonSubmenu extends MenuPopupHelper {
426 private SubMenuBuilder mSubMenu;
427
428 public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu) {
429 super(context, subMenu);
430 mSubMenu = subMenu;
431
432 MenuItemImpl item = (MenuItemImpl) subMenu.getItem();
433 if (!item.isActionButton()) {
434 // Give a reasonable anchor to nested submenus.
435 setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton);
436 }
437 }
438
439 @Override
440 public void onDismiss() {
441 super.onDismiss();
442 mSubMenu.close();
443 mActionButtonPopup = null;
444 }
445 }
446
447 private class OpenOverflowRunnable implements Runnable {
448 private OverflowPopup mPopup;
449
450 public OpenOverflowRunnable(OverflowPopup popup) {
451 mPopup = popup;
452 }
453
454 public void run() {
455 mMenu.changeMenuMode();
456 if (mPopup.tryShow()) {
457 mOverflowPopup = mPopup;
458 mPostedOpenRunnable = null;
459 }
460 }
461 }
462}