blob: 0051ec3a1b44a50849e095065b9cec84e9260276 [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) {
115 final View actionView = item.getActionView();
116 return actionView != null ? actionView : super.getItemView(item, convertView, parent);
117 }
118
119 @Override
120 public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) {
121 itemView.initialize(item, 0);
122 ((ActionMenuItemView) itemView).setItemInvoker((ActionMenuView) mMenuView);
123 }
124
125 @Override
126 public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) {
127 return item.isActionButton();
128 }
129
130 @Override
131 public void updateMenuView(boolean cleared) {
132 super.updateMenuView(cleared);
133
134 if (mReserveOverflow && mMenu.getNonActionItems().size() > 0) {
135 if (mOverflowButton == null) {
136 mOverflowButton = new OverflowMenuButton(mContext);
Adam Powell640a66e2011-04-29 10:18:53 -0700137 mOverflowButton.setLayoutParams(
138 ((ActionMenuView) mMenuView).generateOverflowButtonLayoutParams());
Adam Powell696cba52011-03-29 10:38:16 -0700139 }
140 ViewGroup parent = (ViewGroup) mOverflowButton.getParent();
141 if (parent != mMenuView) {
142 if (parent != null) {
143 parent.removeView(mOverflowButton);
144 }
145 ((ViewGroup) mMenuView).addView(mOverflowButton);
146 }
147 } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) {
148 ((ViewGroup) mMenuView).removeView(mOverflowButton);
149 }
150 }
151
152 @Override
153 public boolean filterLeftoverView(ViewGroup parent, int childIndex) {
154 if (parent.getChildAt(childIndex) == mOverflowButton) return false;
155 return super.filterLeftoverView(parent, childIndex);
156 }
157
158 public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
159 if (!subMenu.hasVisibleItems()) return false;
160
161 SubMenuBuilder topSubMenu = subMenu;
162 while (topSubMenu.getParentMenu() != mMenu) {
163 topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu();
164 }
165 View anchor = findViewForItem(topSubMenu.getItem());
166 if (anchor == null) return false;
167
168 mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu);
169 mActionButtonPopup.setAnchorView(anchor);
170 mActionButtonPopup.show();
171 super.onSubMenuSelected(subMenu);
172 return true;
173 }
174
175 private View findViewForItem(MenuItem item) {
176 final ViewGroup parent = (ViewGroup) mMenuView;
177 if (parent == null) return null;
178
179 final int count = parent.getChildCount();
180 for (int i = 0; i < count; i++) {
181 final View child = parent.getChildAt(i);
182 if (child instanceof MenuView.ItemView &&
183 ((MenuView.ItemView) child).getItemData() == item) {
184 return child;
185 }
186 }
187 return null;
188 }
189
190 /**
191 * Display the overflow menu if one is present.
192 * @return true if the overflow menu was shown, false otherwise.
193 */
194 public boolean showOverflowMenu() {
195 if (mReserveOverflow && !isOverflowMenuShowing() && mMenuView != null &&
196 mPostedOpenRunnable == null) {
Adam Powell696cba52011-03-29 10:38:16 -0700197 OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true);
198 mPostedOpenRunnable = new OpenOverflowRunnable(popup);
199 // Post this for later; we might still need a layout for the anchor to be right.
200 ((View) mMenuView).post(mPostedOpenRunnable);
201
202 // ActionMenuPresenter uses null as a callback argument here
203 // to indicate overflow is opening.
204 super.onSubMenuSelected(null);
205
206 return true;
207 }
208 return false;
209 }
210
211 /**
212 * Hide the overflow menu if it is currently showing.
213 *
214 * @return true if the overflow menu was hidden, false otherwise.
215 */
216 public boolean hideOverflowMenu() {
217 if (mPostedOpenRunnable != null && mMenuView != null) {
218 ((View) mMenuView).removeCallbacks(mPostedOpenRunnable);
219 return true;
220 }
221
222 MenuPopupHelper popup = mOverflowPopup;
223 if (popup != null) {
224 popup.dismiss();
225 return true;
226 }
227 return false;
228 }
229
230 /**
231 * Dismiss all popup menus - overflow and submenus.
232 * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
233 */
234 public boolean dismissPopupMenus() {
235 boolean result = hideOverflowMenu();
236 result |= hideSubMenus();
237 return result;
238 }
239
240 /**
241 * Dismiss all submenu popups.
242 *
243 * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
244 */
245 public boolean hideSubMenus() {
246 if (mActionButtonPopup != null) {
247 mActionButtonPopup.dismiss();
248 return true;
249 }
250 return false;
251 }
252
253 /**
254 * @return true if the overflow menu is currently showing
255 */
256 public boolean isOverflowMenuShowing() {
257 return mOverflowPopup != null && mOverflowPopup.isShowing();
258 }
259
260 /**
261 * @return true if space has been reserved in the action menu for an overflow item.
262 */
263 public boolean isOverflowReserved() {
264 return mReserveOverflow;
265 }
266
267 public boolean flagActionItems() {
268 final ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems();
269 final int itemsSize = visibleItems.size();
270 int maxActions = mMaxItems;
271 int widthLimit = mActionItemWidthLimit;
272 final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
273 final ViewGroup parent = (ViewGroup) mMenuView;
274
275 int requiredItems = 0;
276 int requestedItems = 0;
277 int firstActionWidth = 0;
278 boolean hasOverflow = false;
279 for (int i = 0; i < itemsSize; i++) {
280 MenuItemImpl item = visibleItems.get(i);
281 if (item.requiresActionButton()) {
282 requiredItems++;
283 } else if (item.requestsActionButton()) {
284 requestedItems++;
285 } else {
286 hasOverflow = true;
287 }
288 }
289
290 // Reserve a spot for the overflow item if needed.
291 if (mReserveOverflow &&
292 (hasOverflow || requiredItems + requestedItems > maxActions)) {
293 maxActions--;
294 }
295 maxActions -= requiredItems;
296
297 final SparseBooleanArray seenGroups = mActionButtonGroups;
298 seenGroups.clear();
299
300 // Flag as many more requested items as will fit.
301 for (int i = 0; i < itemsSize; i++) {
302 MenuItemImpl item = visibleItems.get(i);
303
304 if (item.requiresActionButton()) {
305 View v = item.getActionView();
306 if (v == null) {
307 v = getItemView(item, mScrapActionButtonView, parent);
308 if (mScrapActionButtonView == null) {
309 mScrapActionButtonView = v;
310 }
311 }
312 v.measure(querySpec, querySpec);
313 final int measuredWidth = v.getMeasuredWidth();
314 widthLimit -= measuredWidth;
315 if (firstActionWidth == 0) {
316 firstActionWidth = measuredWidth;
317 }
318 final int groupId = item.getGroupId();
319 if (groupId != 0) {
320 seenGroups.put(groupId, true);
321 }
322 } else if (item.requestsActionButton()) {
323 // Items in a group with other items that already have an action slot
324 // can break the max actions rule, but not the width limit.
325 final int groupId = item.getGroupId();
326 final boolean inGroup = seenGroups.get(groupId);
327 boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0;
328 maxActions--;
329
330 if (isAction) {
331 View v = item.getActionView();
332 if (v == null) {
333 v = getItemView(item, mScrapActionButtonView, parent);
334 if (mScrapActionButtonView == null) {
335 mScrapActionButtonView = v;
336 }
337 }
338 v.measure(querySpec, querySpec);
339 final int measuredWidth = v.getMeasuredWidth();
340 widthLimit -= measuredWidth;
341 if (firstActionWidth == 0) {
342 firstActionWidth = measuredWidth;
343 }
344
Adam Powell640a66e2011-04-29 10:18:53 -0700345 if (mStrictWidthLimit) {
346 isAction = widthLimit >= 0;
Adam Powell640a66e2011-04-29 10:18:53 -0700347 } else {
348 // Did this push the entire first item past the limit?
349 isAction = widthLimit + firstActionWidth > 0;
Adam Powell696cba52011-03-29 10:38:16 -0700350 }
351 }
352
353 if (isAction && groupId != 0) {
354 seenGroups.put(groupId, true);
355 } else if (inGroup) {
356 // We broke the width limit. Demote the whole group, they all overflow now.
357 seenGroups.put(groupId, false);
358 for (int j = 0; j < i; j++) {
359 MenuItemImpl areYouMyGroupie = visibleItems.get(j);
360 if (areYouMyGroupie.getGroupId() == groupId) {
361 areYouMyGroupie.setIsActionButton(false);
362 }
363 }
364 }
365
366 item.setIsActionButton(isAction);
367 }
368 }
369 return true;
370 }
371
372 @Override
373 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
374 dismissPopupMenus();
375 super.onCloseMenu(menu, allMenusAreClosing);
376 }
377
378 private class OverflowMenuButton extends ImageButton implements ActionMenuChildView {
379 public OverflowMenuButton(Context context) {
380 super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle);
381
382 setClickable(true);
383 setFocusable(true);
384 setVisibility(VISIBLE);
385 setEnabled(true);
386 }
387
388 @Override
389 public boolean performClick() {
390 if (super.performClick()) {
391 return true;
392 }
393
394 playSoundEffect(SoundEffectConstants.CLICK);
395 showOverflowMenu();
396 return true;
397 }
398
399 public boolean needsDividerBefore() {
400 return true;
401 }
402
403 public boolean needsDividerAfter() {
404 return false;
405 }
406 }
407
408 private class OverflowPopup extends MenuPopupHelper {
409 public OverflowPopup(Context context, MenuBuilder menu, View anchorView,
410 boolean overflowOnly) {
411 super(context, menu, anchorView, overflowOnly);
412 }
413
414 @Override
415 public void onDismiss() {
416 super.onDismiss();
417 mMenu.close();
418 mOverflowPopup = null;
419 }
420 }
421
422 private class ActionButtonSubmenu extends MenuPopupHelper {
423 private SubMenuBuilder mSubMenu;
424
425 public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu) {
426 super(context, subMenu);
427 mSubMenu = subMenu;
428
429 MenuItemImpl item = (MenuItemImpl) subMenu.getItem();
430 if (!item.isActionButton()) {
431 // Give a reasonable anchor to nested submenus.
432 setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton);
433 }
434 }
435
436 @Override
437 public void onDismiss() {
438 super.onDismiss();
439 mSubMenu.close();
440 mActionButtonPopup = null;
441 }
442 }
443
444 private class OpenOverflowRunnable implements Runnable {
445 private OverflowPopup mPopup;
446
447 public OpenOverflowRunnable(OverflowPopup popup) {
448 mPopup = popup;
449 }
450
451 public void run() {
452 mMenu.changeMenuMode();
453 if (mPopup.tryShow()) {
454 mOverflowPopup = mPopup;
455 mPostedOpenRunnable = null;
456 }
457 }
458 }
459}