blob: e9fcb23113c6bdd0ffcf9d1d44da5912f2faf9dd [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2006 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
19
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.pm.PackageManager;
24import android.content.pm.ResolveInfo;
25import android.content.res.Configuration;
26import android.content.res.Resources;
27import android.graphics.drawable.Drawable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028import android.os.Parcelable;
Adam Powell696cba52011-03-29 10:38:16 -070029import android.util.Log;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080030import android.util.SparseArray;
Adam Powell36fced92011-01-16 15:48:07 -080031import android.view.ContextMenu.ContextMenuInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080032import android.view.KeyCharacterMap;
33import android.view.KeyEvent;
34import android.view.Menu;
35import android.view.MenuItem;
36import android.view.SubMenu;
37import android.view.View;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080038
39import java.lang.ref.WeakReference;
40import java.util.ArrayList;
Adam Powell696cba52011-03-29 10:38:16 -070041import java.util.Iterator;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080042import java.util.List;
Adam Powell696cba52011-03-29 10:38:16 -070043import java.util.concurrent.CopyOnWriteArrayList;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080044
45/**
46 * Implementation of the {@link android.view.Menu} interface for creating a
47 * standard menu UI.
48 */
49public class MenuBuilder implements Menu {
50 private static final String LOGTAG = "MenuBuilder";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051
52 private static final int[] sCategoryToOrder = new int[] {
53 1, /* No category */
54 4, /* CONTAINER */
55 5, /* SYSTEM */
56 3, /* SECONDARY */
57 2, /* ALTERNATIVE */
58 0, /* SELECTED_ALTERNATIVE */
59 };
60
61 private final Context mContext;
62 private final Resources mResources;
63
64 /**
65 * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode()
66 * instead of accessing this directly.
67 */
68 private boolean mQwertyMode;
69
70 /**
71 * Whether the shortcuts should be visible on menus. Use isShortcutsVisible()
72 * instead of accessing this directly.
73 */
74 private boolean mShortcutsVisible;
75
76 /**
77 * Callback that will receive the various menu-related events generated by
78 * this class. Use getCallback to get a reference to the callback.
79 */
80 private Callback mCallback;
81
82 /** Contains all of the items for this menu */
83 private ArrayList<MenuItemImpl> mItems;
84
85 /** Contains only the items that are currently visible. This will be created/refreshed from
86 * {@link #getVisibleItems()} */
87 private ArrayList<MenuItemImpl> mVisibleItems;
88 /**
89 * Whether or not the items (or any one item's shown state) has changed since it was last
90 * fetched from {@link #getVisibleItems()}
91 */
92 private boolean mIsVisibleItemsStale;
Adam Powell96675b12010-06-10 18:58:59 -070093
94 /**
95 * Contains only the items that should appear in the Action Bar, if present.
96 */
97 private ArrayList<MenuItemImpl> mActionItems;
98 /**
99 * Contains items that should NOT appear in the Action Bar, if present.
100 */
101 private ArrayList<MenuItemImpl> mNonActionItems;
Adam Powell696cba52011-03-29 10:38:16 -0700102
Adam Powell36fced92011-01-16 15:48:07 -0800103 /**
Adam Powell96675b12010-06-10 18:58:59 -0700104 * Whether or not the items (or any one item's action state) has changed since it was
105 * last fetched.
106 */
107 private boolean mIsActionItemsStale;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800108
109 /**
Adam Powell4d9861e2010-08-17 11:14:40 -0700110 * Default value for how added items should show in the action list.
111 */
112 private int mDefaultShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER;
113
114 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800115 * Current use case is Context Menus: As Views populate the context menu, each one has
116 * extra information that should be passed along. This is the current menu info that
117 * should be set on all items added to this menu.
118 */
119 private ContextMenuInfo mCurrentMenuInfo;
120
121 /** Header title for menu types that have a header (context and submenus) */
122 CharSequence mHeaderTitle;
123 /** Header icon for menu types that have a header and support icons (context) */
124 Drawable mHeaderIcon;
125 /** Header custom view for menu types that have a header and support custom views (context) */
126 View mHeaderView;
127
128 /**
129 * Contains the state of the View hierarchy for all menu views when the menu
130 * was frozen.
131 */
132 private SparseArray<Parcelable> mFrozenViewStates;
133
134 /**
135 * Prevents onItemsChanged from doing its junk, useful for batching commands
136 * that may individually call onItemsChanged.
137 */
138 private boolean mPreventDispatchingItemsChanged = false;
Adam Powell696cba52011-03-29 10:38:16 -0700139 private boolean mItemsChangedWhileDispatchPrevented = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800140
141 private boolean mOptionalIconsVisible = false;
Adam Powell2fbf4de62010-09-30 15:46:46 -0700142
Adam Powell696cba52011-03-29 10:38:16 -0700143 private boolean mIsClosing = false;
Adam Powell36fced92011-01-16 15:48:07 -0800144
Adam Powell696cba52011-03-29 10:38:16 -0700145 private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>();
Adam Powell1821ff92011-01-24 21:44:28 -0800146
Adam Powell696cba52011-03-29 10:38:16 -0700147 private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters =
148 new CopyOnWriteArrayList<WeakReference<MenuPresenter>>();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800149
150 /**
Adam Powell696cba52011-03-29 10:38:16 -0700151 * Called by menu to notify of close and selection changes.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800152 */
153 public interface Callback {
154 /**
155 * Called when a menu item is selected.
156 * @param menu The menu that is the parent of the item
157 * @param item The menu item that is selected
158 * @return whether the menu item selection was handled
159 */
160 public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item);
161
162 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800163 * Called when the mode of the menu changes (for example, from icon to expanded).
164 *
165 * @param menu the menu that has changed modes
166 */
167 public void onMenuModeChange(MenuBuilder menu);
168 }
169
170 /**
171 * Called by menu items to execute their associated action
172 */
173 public interface ItemInvoker {
174 public boolean invokeItem(MenuItemImpl item);
175 }
176
177 public MenuBuilder(Context context) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800178 mContext = context;
179 mResources = context.getResources();
180
181 mItems = new ArrayList<MenuItemImpl>();
182
183 mVisibleItems = new ArrayList<MenuItemImpl>();
184 mIsVisibleItemsStale = true;
185
Adam Powell96675b12010-06-10 18:58:59 -0700186 mActionItems = new ArrayList<MenuItemImpl>();
187 mNonActionItems = new ArrayList<MenuItemImpl>();
188 mIsActionItemsStale = true;
189
Jeff Brown4aed78b2011-01-14 17:36:55 -0800190 setShortcutsVisibleInner(true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800191 }
192
Adam Powell4d9861e2010-08-17 11:14:40 -0700193 public MenuBuilder setDefaultShowAsAction(int defaultShowAsAction) {
194 mDefaultShowAsAction = defaultShowAsAction;
195 return this;
196 }
Adam Powell696cba52011-03-29 10:38:16 -0700197
198 /**
199 * Add a presenter to this menu. This will only hold a WeakReference;
200 * you do not need to explicitly remove a presenter, but you can using
201 * {@link #removeMenuPresenter(MenuPresenter)}.
202 *
203 * @param presenter The presenter to add
204 */
205 public void addMenuPresenter(MenuPresenter presenter) {
206 mPresenters.add(new WeakReference<MenuPresenter>(presenter));
207 presenter.initForMenu(mContext, this);
208 mIsActionItemsStale = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800209 }
210
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800211 /**
Adam Powell696cba52011-03-29 10:38:16 -0700212 * Remove a presenter from this menu. That presenter will no longer
213 * receive notifications of updates to this menu's data.
214 *
215 * @param presenter The presenter to remove
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800216 */
Adam Powell696cba52011-03-29 10:38:16 -0700217 public void removeMenuPresenter(MenuPresenter presenter) {
218 for (WeakReference<MenuPresenter> ref : mPresenters) {
219 final MenuPresenter item = ref.get();
220 if (item == null || item == presenter) {
221 mPresenters.remove(ref);
222 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800223 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800224 }
225
Adam Powell696cba52011-03-29 10:38:16 -0700226 private void dispatchPresenterUpdate(boolean cleared) {
227 if (mPresenters.isEmpty()) return;
228
Adam Powell640a66e2011-04-29 10:18:53 -0700229 stopDispatchingItemsChanged();
Adam Powell696cba52011-03-29 10:38:16 -0700230 for (WeakReference<MenuPresenter> ref : mPresenters) {
231 final MenuPresenter presenter = ref.get();
232 if (presenter == null) {
233 mPresenters.remove(ref);
234 } else {
235 presenter.updateMenuView(cleared);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800236 }
237 }
Adam Powell640a66e2011-04-29 10:18:53 -0700238 startDispatchingItemsChanged();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800239 }
240
Adam Powell696cba52011-03-29 10:38:16 -0700241 private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu) {
242 if (mPresenters.isEmpty()) return false;
243
244 boolean result = false;
245
246 for (WeakReference<MenuPresenter> ref : mPresenters) {
247 final MenuPresenter presenter = ref.get();
248 if (presenter == null) {
249 mPresenters.remove(ref);
250 } else if (!result) {
251 result = presenter.onSubMenuSelected(subMenu);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800252 }
253 }
Adam Powell696cba52011-03-29 10:38:16 -0700254 return result;
255 }
256
257 public void setCallback(Callback cb) {
258 mCallback = cb;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800259 }
260
261 /**
262 * Adds an item to the menu. The other add methods funnel to this.
263 */
264 private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
265 final int ordering = getOrdering(categoryOrder);
266
Adam Powell4d9861e2010-08-17 11:14:40 -0700267 final MenuItemImpl item = new MenuItemImpl(this, group, id, categoryOrder,
268 ordering, title, mDefaultShowAsAction);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800269
270 if (mCurrentMenuInfo != null) {
271 // Pass along the current menu info
272 item.setMenuInfo(mCurrentMenuInfo);
273 }
274
275 mItems.add(findInsertIndex(mItems, ordering), item);
Adam Powell696cba52011-03-29 10:38:16 -0700276 onItemsChanged(true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800277
278 return item;
279 }
280
281 public MenuItem add(CharSequence title) {
282 return addInternal(0, 0, 0, title);
283 }
284
285 public MenuItem add(int titleRes) {
286 return addInternal(0, 0, 0, mResources.getString(titleRes));
287 }
288
289 public MenuItem add(int group, int id, int categoryOrder, CharSequence title) {
290 return addInternal(group, id, categoryOrder, title);
291 }
292
293 public MenuItem add(int group, int id, int categoryOrder, int title) {
294 return addInternal(group, id, categoryOrder, mResources.getString(title));
295 }
296
297 public SubMenu addSubMenu(CharSequence title) {
298 return addSubMenu(0, 0, 0, title);
299 }
300
301 public SubMenu addSubMenu(int titleRes) {
302 return addSubMenu(0, 0, 0, mResources.getString(titleRes));
303 }
304
305 public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
306 final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title);
307 final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item);
308 item.setSubMenu(subMenu);
309
310 return subMenu;
311 }
312
313 public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) {
314 return addSubMenu(group, id, categoryOrder, mResources.getString(title));
315 }
316
317 public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller,
318 Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) {
319 PackageManager pm = mContext.getPackageManager();
320 final List<ResolveInfo> lri =
321 pm.queryIntentActivityOptions(caller, specifics, intent, 0);
322 final int N = lri != null ? lri.size() : 0;
323
324 if ((flags & FLAG_APPEND_TO_GROUP) == 0) {
325 removeGroup(group);
326 }
327
328 for (int i=0; i<N; i++) {
329 final ResolveInfo ri = lri.get(i);
330 Intent rintent = new Intent(
331 ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]);
332 rintent.setComponent(new ComponentName(
333 ri.activityInfo.applicationInfo.packageName,
334 ri.activityInfo.name));
335 final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm))
336 .setIcon(ri.loadIcon(pm))
337 .setIntent(rintent);
338 if (outSpecificItems != null && ri.specificIndex >= 0) {
339 outSpecificItems[ri.specificIndex] = item;
340 }
341 }
342
343 return N;
344 }
345
346 public void removeItem(int id) {
347 removeItemAtInt(findItemIndex(id), true);
348 }
349
350 public void removeGroup(int group) {
351 final int i = findGroupIndex(group);
352
353 if (i >= 0) {
354 final int maxRemovable = mItems.size() - i;
355 int numRemoved = 0;
356 while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) {
357 // Don't force update for each one, this method will do it at the end
358 removeItemAtInt(i, false);
359 }
360
361 // Notify menu views
Adam Powell696cba52011-03-29 10:38:16 -0700362 onItemsChanged(true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800363 }
364 }
365
366 /**
367 * Remove the item at the given index and optionally forces menu views to
368 * update.
369 *
370 * @param index The index of the item to be removed. If this index is
371 * invalid an exception is thrown.
372 * @param updateChildrenOnMenuViews Whether to force update on menu views.
373 * Please make sure you eventually call this after your batch of
374 * removals.
375 */
376 private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) {
377 if ((index < 0) || (index >= mItems.size())) return;
378
379 mItems.remove(index);
380
Adam Powell696cba52011-03-29 10:38:16 -0700381 if (updateChildrenOnMenuViews) onItemsChanged(true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800382 }
383
384 public void removeItemAt(int index) {
385 removeItemAtInt(index, true);
386 }
387
388 public void clearAll() {
389 mPreventDispatchingItemsChanged = true;
390 clear();
391 clearHeader();
392 mPreventDispatchingItemsChanged = false;
Adam Powell696cba52011-03-29 10:38:16 -0700393 mItemsChangedWhileDispatchPrevented = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800394 onItemsChanged(true);
395 }
396
397 public void clear() {
398 mItems.clear();
399
400 onItemsChanged(true);
401 }
402
403 void setExclusiveItemChecked(MenuItem item) {
404 final int group = item.getGroupId();
405
406 final int N = mItems.size();
407 for (int i = 0; i < N; i++) {
408 MenuItemImpl curItem = mItems.get(i);
409 if (curItem.getGroupId() == group) {
410 if (!curItem.isExclusiveCheckable()) continue;
411 if (!curItem.isCheckable()) continue;
412
413 // Check the item meant to be checked, uncheck the others (that are in the group)
414 curItem.setCheckedInt(curItem == item);
415 }
416 }
417 }
418
419 public void setGroupCheckable(int group, boolean checkable, boolean exclusive) {
420 final int N = mItems.size();
421
422 for (int i = 0; i < N; i++) {
423 MenuItemImpl item = mItems.get(i);
424 if (item.getGroupId() == group) {
425 item.setExclusiveCheckable(exclusive);
426 item.setCheckable(checkable);
427 }
428 }
429 }
430
431 public void setGroupVisible(int group, boolean visible) {
432 final int N = mItems.size();
433
434 // We handle the notification of items being changed ourselves, so we use setVisibleInt rather
435 // than setVisible and at the end notify of items being changed
436
437 boolean changedAtLeastOneItem = false;
438 for (int i = 0; i < N; i++) {
439 MenuItemImpl item = mItems.get(i);
440 if (item.getGroupId() == group) {
441 if (item.setVisibleInt(visible)) changedAtLeastOneItem = true;
442 }
443 }
444
445 if (changedAtLeastOneItem) onItemsChanged(false);
446 }
447
448 public void setGroupEnabled(int group, boolean enabled) {
449 final int N = mItems.size();
450
451 for (int i = 0; i < N; i++) {
452 MenuItemImpl item = mItems.get(i);
453 if (item.getGroupId() == group) {
454 item.setEnabled(enabled);
455 }
456 }
457 }
458
459 public boolean hasVisibleItems() {
460 final int size = size();
461
462 for (int i = 0; i < size; i++) {
463 MenuItemImpl item = mItems.get(i);
464 if (item.isVisible()) {
465 return true;
466 }
467 }
468
469 return false;
470 }
471
472 public MenuItem findItem(int id) {
473 final int size = size();
474 for (int i = 0; i < size; i++) {
475 MenuItemImpl item = mItems.get(i);
476 if (item.getItemId() == id) {
477 return item;
478 } else if (item.hasSubMenu()) {
479 MenuItem possibleItem = item.getSubMenu().findItem(id);
480
481 if (possibleItem != null) {
482 return possibleItem;
483 }
484 }
485 }
486
487 return null;
488 }
489
490 public int findItemIndex(int id) {
491 final int size = size();
492
493 for (int i = 0; i < size; i++) {
494 MenuItemImpl item = mItems.get(i);
495 if (item.getItemId() == id) {
496 return i;
497 }
498 }
499
500 return -1;
501 }
502
503 public int findGroupIndex(int group) {
504 return findGroupIndex(group, 0);
505 }
506
507 public int findGroupIndex(int group, int start) {
508 final int size = size();
509
510 if (start < 0) {
511 start = 0;
512 }
513
514 for (int i = start; i < size; i++) {
515 final MenuItemImpl item = mItems.get(i);
516
517 if (item.getGroupId() == group) {
518 return i;
519 }
520 }
521
522 return -1;
523 }
524
525 public int size() {
526 return mItems.size();
527 }
528
529 /** {@inheritDoc} */
530 public MenuItem getItem(int index) {
531 return mItems.get(index);
532 }
533
534 public boolean isShortcutKey(int keyCode, KeyEvent event) {
535 return findItemWithShortcutForKey(keyCode, event) != null;
536 }
537
538 public void setQwertyMode(boolean isQwerty) {
539 mQwertyMode = isQwerty;
Adam Powell696cba52011-03-29 10:38:16 -0700540
541 onItemsChanged(false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800542 }
543
544 /**
545 * Returns the ordering across all items. This will grab the category from
546 * the upper bits, find out how to order the category with respect to other
547 * categories, and combine it with the lower bits.
548 *
549 * @param categoryOrder The category order for a particular item (if it has
550 * not been or/add with a category, the default category is
551 * assumed).
552 * @return An ordering integer that can be used to order this item across
553 * all the items (even from other categories).
554 */
Adam Powell696cba52011-03-29 10:38:16 -0700555 private static int getOrdering(int categoryOrder) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800556 final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT;
557
558 if (index < 0 || index >= sCategoryToOrder.length) {
559 throw new IllegalArgumentException("order does not contain a valid category.");
560 }
561
562 return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK);
563 }
564
565 /**
566 * @return whether the menu shortcuts are in qwerty mode or not
567 */
568 boolean isQwertyMode() {
569 return mQwertyMode;
570 }
571
572 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800573 * Sets whether the shortcuts should be visible on menus. Devices without hardware
574 * key input will never make shortcuts visible even if this method is passed 'true'.
575 *
576 * @param shortcutsVisible Whether shortcuts should be visible (if true and a
577 * menu item does not have a shortcut defined, that item will
578 * still NOT show a shortcut)
579 */
580 public void setShortcutsVisible(boolean shortcutsVisible) {
581 if (mShortcutsVisible == shortcutsVisible) return;
Jeff Brown4aed78b2011-01-14 17:36:55 -0800582
583 setShortcutsVisibleInner(shortcutsVisible);
Adam Powell696cba52011-03-29 10:38:16 -0700584 onItemsChanged(false);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800585 }
586
Jeff Brown4aed78b2011-01-14 17:36:55 -0800587 private void setShortcutsVisibleInner(boolean shortcutsVisible) {
588 mShortcutsVisible = shortcutsVisible
589 && mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS
590 && mResources.getBoolean(
591 com.android.internal.R.bool.config_showMenuShortcutsWhenKeyboardPresent);
592 }
593
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800594 /**
595 * @return Whether shortcuts should be visible on menus.
596 */
597 public boolean isShortcutsVisible() {
598 return mShortcutsVisible;
599 }
600
601 Resources getResources() {
602 return mResources;
603 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800604
605 public Context getContext() {
606 return mContext;
607 }
608
Adam Powell696cba52011-03-29 10:38:16 -0700609 boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) {
610 return mCallback != null && mCallback.onMenuItemSelected(menu, item);
611 }
612
613 /**
614 * Dispatch a mode change event to this menu's callback.
615 */
616 public void changeMenuMode() {
617 if (mCallback != null) {
618 mCallback.onMenuModeChange(this);
619 }
620 }
621
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800622 private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) {
623 for (int i = items.size() - 1; i >= 0; i--) {
624 MenuItemImpl item = items.get(i);
625 if (item.getOrdering() <= ordering) {
626 return i + 1;
627 }
628 }
629
630 return 0;
631 }
632
633 public boolean performShortcut(int keyCode, KeyEvent event, int flags) {
634 final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event);
635
636 boolean handled = false;
637
638 if (item != null) {
639 handled = performItemAction(item, flags);
640 }
641
642 if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) {
643 close(true);
644 }
645
646 return handled;
647 }
648
Nicolas Roarde0fc8382009-09-24 21:30:06 +0100649 /*
650 * This function will return all the menu and sub-menu items that can
651 * be directly (the shortcut directly corresponds) and indirectly
652 * (the ALT-enabled char corresponds to the shortcut) associated
653 * with the keyCode.
654 */
Adam Powell696cba52011-03-29 10:38:16 -0700655 void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800656 final boolean qwerty = isQwertyMode();
657 final int metaState = event.getMetaState();
658 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
659 // Get the chars associated with the keyCode (i.e using any chording combo)
660 final boolean isKeyCodeMapped = event.getKeyData(possibleChars);
661 // The delete key is not mapped to '\b' so we treat it specially
662 if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) {
Adam Powell696cba52011-03-29 10:38:16 -0700663 return;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800664 }
665
666 // Look for an item whose shortcut is this key.
667 final int N = mItems.size();
668 for (int i = 0; i < N; i++) {
669 MenuItemImpl item = mItems.get(i);
670 if (item.hasSubMenu()) {
Adam Powell696cba52011-03-29 10:38:16 -0700671 ((MenuBuilder)item.getSubMenu()).findItemsWithShortcutForKey(items, keyCode, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800672 }
Nicolas Roarde0fc8382009-09-24 21:30:06 +0100673 final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut();
674 if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) &&
675 (shortcutChar != 0) &&
676 (shortcutChar == possibleChars.meta[0]
677 || shortcutChar == possibleChars.meta[2]
678 || (qwerty && shortcutChar == '\b' &&
679 keyCode == KeyEvent.KEYCODE_DEL)) &&
680 item.isEnabled()) {
681 items.add(item);
682 }
683 }
Nicolas Roarde0fc8382009-09-24 21:30:06 +0100684 }
685
686 /*
687 * We want to return the menu item associated with the key, but if there is no
688 * ambiguity (i.e. there is only one menu item corresponding to the key) we want
689 * to return it even if it's not an exact match; this allow the user to
690 * _not_ use the ALT key for example, making the use of shortcuts slightly more
691 * user-friendly. An example is on the G1, '!' and '1' are on the same key, and
692 * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut).
693 *
694 * On the other hand, if two (or more) shortcuts corresponds to the same key,
695 * we have to only return the exact match.
696 */
697 MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) {
698 // Get all items that can be associated directly or indirectly with the keyCode
Adam Powell696cba52011-03-29 10:38:16 -0700699 ArrayList<MenuItemImpl> items = mTempShortcutItemList;
700 items.clear();
701 findItemsWithShortcutForKey(items, keyCode, event);
Nicolas Roarde0fc8382009-09-24 21:30:06 +0100702
Adam Powell696cba52011-03-29 10:38:16 -0700703 if (items.isEmpty()) {
Nicolas Roarde0fc8382009-09-24 21:30:06 +0100704 return null;
705 }
706
707 final int metaState = event.getMetaState();
708 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
709 // Get the chars associated with the keyCode (i.e using any chording combo)
710 event.getKeyData(possibleChars);
711
712 // If we have only one element, we can safely returns it
Adam Powell696cba52011-03-29 10:38:16 -0700713 final int size = items.size();
714 if (size == 1) {
Nicolas Roarde0fc8382009-09-24 21:30:06 +0100715 return items.get(0);
716 }
717
718 final boolean qwerty = isQwertyMode();
719 // If we found more than one item associated with the key,
720 // we have to return the exact match
Adam Powell696cba52011-03-29 10:38:16 -0700721 for (int i = 0; i < size; i++) {
722 final MenuItemImpl item = items.get(i);
723 final char shortcutChar = qwerty ? item.getAlphabeticShortcut() :
724 item.getNumericShortcut();
Nicolas Roarde0fc8382009-09-24 21:30:06 +0100725 if ((shortcutChar == possibleChars.meta[0] &&
726 (metaState & KeyEvent.META_ALT_ON) == 0)
727 || (shortcutChar == possibleChars.meta[2] &&
728 (metaState & KeyEvent.META_ALT_ON) != 0)
729 || (qwerty && shortcutChar == '\b' &&
730 keyCode == KeyEvent.KEYCODE_DEL)) {
731 return item;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800732 }
733 }
734 return null;
735 }
Nicolas Roarde0fc8382009-09-24 21:30:06 +0100736
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800737 public boolean performIdentifierAction(int id, int flags) {
738 // Look for an item whose identifier is the id.
739 return performItemAction(findItem(id), flags);
740 }
741
742 public boolean performItemAction(MenuItem item, int flags) {
743 MenuItemImpl itemImpl = (MenuItemImpl) item;
744
745 if (itemImpl == null || !itemImpl.isEnabled()) {
746 return false;
747 }
748
749 boolean invoked = itemImpl.invoke();
750
751 if (item.hasSubMenu()) {
752 close(false);
753
Adam Powell696cba52011-03-29 10:38:16 -0700754 invoked |= dispatchSubMenuSelected((SubMenuBuilder) item.getSubMenu());
755 if (!invoked) close(true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800756 } else {
757 if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) {
758 close(true);
759 }
760 }
761
762 return invoked;
763 }
764
765 /**
766 * Closes the visible menu.
767 *
768 * @param allMenusAreClosing Whether the menus are completely closing (true),
769 * or whether there is another menu coming in this menu's place
770 * (false). For example, if the menu is closing because a
771 * sub menu is about to be shown, <var>allMenusAreClosing</var>
772 * is false.
773 */
774 final void close(boolean allMenusAreClosing) {
Adam Powell696cba52011-03-29 10:38:16 -0700775 if (mIsClosing) return;
776
777 mIsClosing = true;
778 for (WeakReference<MenuPresenter> ref : mPresenters) {
779 final MenuPresenter presenter = ref.get();
780 if (presenter == null) {
781 mPresenters.remove(ref);
782 } else {
783 presenter.onCloseMenu(this, allMenusAreClosing);
784 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800785 }
Adam Powell696cba52011-03-29 10:38:16 -0700786 mIsClosing = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800787 }
788
789 /** {@inheritDoc} */
790 public void close() {
791 close(true);
792 }
793
794 /**
795 * Called when an item is added or removed.
796 *
Adam Powell696cba52011-03-29 10:38:16 -0700797 * @param structureChanged true if the menu structure changed,
798 * false if only item properties changed.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800799 */
Adam Powell696cba52011-03-29 10:38:16 -0700800 void onItemsChanged(boolean structureChanged) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800801 if (!mPreventDispatchingItemsChanged) {
Adam Powell696cba52011-03-29 10:38:16 -0700802 if (structureChanged) {
803 mIsVisibleItemsStale = true;
804 mIsActionItemsStale = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800805 }
Adam Powell696cba52011-03-29 10:38:16 -0700806
807 dispatchPresenterUpdate(structureChanged);
808 } else {
809 mItemsChangedWhileDispatchPrevented = true;
810 }
811 }
812
813 /**
814 * Stop dispatching item changed events to presenters until
815 * {@link #startDispatchingItemsChanged()} is called. Useful when
816 * many menu operations are going to be performed as a batch.
817 */
818 public void stopDispatchingItemsChanged() {
Adam Powella86b3502011-04-21 14:49:23 -0700819 if (!mPreventDispatchingItemsChanged) {
820 mPreventDispatchingItemsChanged = true;
821 mItemsChangedWhileDispatchPrevented = false;
822 }
Adam Powell696cba52011-03-29 10:38:16 -0700823 }
824
825 public void startDispatchingItemsChanged() {
826 mPreventDispatchingItemsChanged = false;
827
828 if (mItemsChangedWhileDispatchPrevented) {
829 mItemsChangedWhileDispatchPrevented = false;
830 onItemsChanged(true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800831 }
832 }
833
834 /**
835 * Called by {@link MenuItemImpl} when its visible flag is changed.
836 * @param item The item that has gone through a visibility change.
837 */
838 void onItemVisibleChanged(MenuItemImpl item) {
839 // Notify of items being changed
Adam Powell696cba52011-03-29 10:38:16 -0700840 mIsVisibleItemsStale = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800841 onItemsChanged(false);
842 }
843
Adam Powell96675b12010-06-10 18:58:59 -0700844 /**
845 * Called by {@link MenuItemImpl} when its action request status is changed.
846 * @param item The item that has gone through a change in action request status.
847 */
848 void onItemActionRequestChanged(MenuItemImpl item) {
849 // Notify of items being changed
Adam Powell696cba52011-03-29 10:38:16 -0700850 mIsActionItemsStale = true;
Adam Powell96675b12010-06-10 18:58:59 -0700851 onItemsChanged(false);
852 }
853
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800854 ArrayList<MenuItemImpl> getVisibleItems() {
855 if (!mIsVisibleItemsStale) return mVisibleItems;
856
857 // Refresh the visible items
858 mVisibleItems.clear();
859
860 final int itemsSize = mItems.size();
861 MenuItemImpl item;
862 for (int i = 0; i < itemsSize; i++) {
863 item = mItems.get(i);
864 if (item.isVisible()) mVisibleItems.add(item);
865 }
866
867 mIsVisibleItemsStale = false;
Adam Powell96675b12010-06-10 18:58:59 -0700868 mIsActionItemsStale = true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800869
870 return mVisibleItems;
871 }
Adam Powell36fced92011-01-16 15:48:07 -0800872
873 /**
874 * This method determines which menu items get to be 'action items' that will appear
875 * in an action bar and which items should be 'overflow items' in a secondary menu.
876 * The rules are as follows:
877 *
878 * <p>Items are considered for inclusion in the order specified within the menu.
879 * There is a limit of mMaxActionItems as a total count, optionally including the overflow
880 * menu button itself. This is a soft limit; if an item shares a group ID with an item
881 * previously included as an action item, the new item will stay with its group and become
882 * an action item itself even if it breaks the max item count limit. This is done to
883 * limit the conceptual complexity of the items presented within an action bar. Only a few
884 * unrelated concepts should be presented to the user in this space, and groups are treated
885 * as a single concept.
886 *
887 * <p>There is also a hard limit of consumed measurable space: mActionWidthLimit. This
888 * limit may be broken by a single item that exceeds the remaining space, but no further
889 * items may be added. If an item that is part of a group cannot fit within the remaining
890 * measured width, the entire group will be demoted to overflow. This is done to ensure room
891 * for navigation and other affordances in the action bar as well as reduce general UI clutter.
892 *
893 * <p>The space freed by demoting a full group cannot be consumed by future menu items.
894 * Once items begin to overflow, all future items become overflow items as well. This is
895 * to avoid inadvertent reordering that may break the app's intended design.
Adam Powell36fced92011-01-16 15:48:07 -0800896 */
Adam Powell696cba52011-03-29 10:38:16 -0700897 public void flagActionItems() {
Adam Powell96675b12010-06-10 18:58:59 -0700898 if (!mIsActionItemsStale) {
899 return;
900 }
Adam Powell8028dd32010-07-15 10:16:33 -0700901
Adam Powell696cba52011-03-29 10:38:16 -0700902 // Presenters flag action items as needed.
903 boolean flagged = false;
904 for (WeakReference<MenuPresenter> ref : mPresenters) {
905 final MenuPresenter presenter = ref.get();
906 if (presenter == null) {
907 mPresenters.remove(ref);
Adam Powell8028dd32010-07-15 10:16:33 -0700908 } else {
Adam Powell696cba52011-03-29 10:38:16 -0700909 flagged |= presenter.flagActionItems();
Adam Powell96675b12010-06-10 18:58:59 -0700910 }
911 }
Adam Powell8028dd32010-07-15 10:16:33 -0700912
Adam Powell696cba52011-03-29 10:38:16 -0700913 if (flagged) {
914 mActionItems.clear();
915 mNonActionItems.clear();
916 ArrayList<MenuItemImpl> visibleItems = getVisibleItems();
917 final int itemsSize = visibleItems.size();
918 for (int i = 0; i < itemsSize; i++) {
919 MenuItemImpl item = visibleItems.get(i);
920 if (item.isActionButton()) {
921 mActionItems.add(item);
922 } else {
923 mNonActionItems.add(item);
Adam Powell36fced92011-01-16 15:48:07 -0800924 }
Adam Powell96675b12010-06-10 18:58:59 -0700925 }
Adam Powell696cba52011-03-29 10:38:16 -0700926 } else if (mActionItems.size() + mNonActionItems.size() != getVisibleItems().size()) {
927 // Nobody flagged anything, but if something doesn't add up then treat everything
928 // as non-action items.
929 // (This happens during a first pass with no action-item presenters.)
930 mActionItems.clear();
931 mNonActionItems.clear();
932 mNonActionItems.addAll(getVisibleItems());
Adam Powell96675b12010-06-10 18:58:59 -0700933 }
Adam Powell96675b12010-06-10 18:58:59 -0700934 mIsActionItemsStale = false;
935 }
936
Adam Powell696cba52011-03-29 10:38:16 -0700937 ArrayList<MenuItemImpl> getActionItems() {
938 flagActionItems();
Adam Powell96675b12010-06-10 18:58:59 -0700939 return mActionItems;
940 }
941
Adam Powell696cba52011-03-29 10:38:16 -0700942 ArrayList<MenuItemImpl> getNonActionItems() {
943 flagActionItems();
Adam Powell96675b12010-06-10 18:58:59 -0700944 return mNonActionItems;
945 }
Adam Powell36fced92011-01-16 15:48:07 -0800946
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800947 public void clearHeader() {
948 mHeaderIcon = null;
949 mHeaderTitle = null;
950 mHeaderView = null;
951
952 onItemsChanged(false);
953 }
954
955 private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes,
956 final Drawable icon, final View view) {
957 final Resources r = getResources();
958
959 if (view != null) {
960 mHeaderView = view;
961
962 // If using a custom view, then the title and icon aren't used
963 mHeaderTitle = null;
964 mHeaderIcon = null;
965 } else {
966 if (titleRes > 0) {
967 mHeaderTitle = r.getText(titleRes);
968 } else if (title != null) {
969 mHeaderTitle = title;
970 }
971
972 if (iconRes > 0) {
973 mHeaderIcon = r.getDrawable(iconRes);
974 } else if (icon != null) {
975 mHeaderIcon = icon;
976 }
977
978 // If using the title or icon, then a custom view isn't used
979 mHeaderView = null;
980 }
981
982 // Notify of change
983 onItemsChanged(false);
984 }
985
986 /**
987 * Sets the header's title. This replaces the header view. Called by the
988 * builder-style methods of subclasses.
989 *
990 * @param title The new title.
991 * @return This MenuBuilder so additional setters can be called.
992 */
993 protected MenuBuilder setHeaderTitleInt(CharSequence title) {
994 setHeaderInternal(0, title, 0, null, null);
995 return this;
996 }
997
998 /**
999 * Sets the header's title. This replaces the header view. Called by the
1000 * builder-style methods of subclasses.
1001 *
1002 * @param titleRes The new title (as a resource ID).
1003 * @return This MenuBuilder so additional setters can be called.
1004 */
1005 protected MenuBuilder setHeaderTitleInt(int titleRes) {
1006 setHeaderInternal(titleRes, null, 0, null, null);
1007 return this;
1008 }
1009
1010 /**
1011 * Sets the header's icon. This replaces the header view. Called by the
1012 * builder-style methods of subclasses.
1013 *
1014 * @param icon The new icon.
1015 * @return This MenuBuilder so additional setters can be called.
1016 */
1017 protected MenuBuilder setHeaderIconInt(Drawable icon) {
1018 setHeaderInternal(0, null, 0, icon, null);
1019 return this;
1020 }
1021
1022 /**
1023 * Sets the header's icon. This replaces the header view. Called by the
1024 * builder-style methods of subclasses.
1025 *
1026 * @param iconRes The new icon (as a resource ID).
1027 * @return This MenuBuilder so additional setters can be called.
1028 */
1029 protected MenuBuilder setHeaderIconInt(int iconRes) {
1030 setHeaderInternal(0, null, iconRes, null, null);
1031 return this;
1032 }
1033
1034 /**
1035 * Sets the header's view. This replaces the title and icon. Called by the
1036 * builder-style methods of subclasses.
1037 *
1038 * @param view The new view.
1039 * @return This MenuBuilder so additional setters can be called.
1040 */
1041 protected MenuBuilder setHeaderViewInt(View view) {
1042 setHeaderInternal(0, null, 0, null, view);
1043 return this;
1044 }
1045
1046 public CharSequence getHeaderTitle() {
1047 return mHeaderTitle;
1048 }
1049
1050 public Drawable getHeaderIcon() {
1051 return mHeaderIcon;
1052 }
1053
1054 public View getHeaderView() {
1055 return mHeaderView;
1056 }
1057
1058 /**
1059 * Gets the root menu (if this is a submenu, find its root menu).
1060 * @return The root menu.
1061 */
1062 public MenuBuilder getRootMenu() {
1063 return this;
1064 }
1065
1066 /**
1067 * Sets the current menu info that is set on all items added to this menu
1068 * (until this is called again with different menu info, in which case that
1069 * one will be added to all subsequent item additions).
1070 *
1071 * @param menuInfo The extra menu information to add.
1072 */
1073 public void setCurrentMenuInfo(ContextMenuInfo menuInfo) {
1074 mCurrentMenuInfo = menuInfo;
1075 }
1076
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001077 void setOptionalIconsVisible(boolean visible) {
1078 mOptionalIconsVisible = visible;
1079 }
1080
1081 boolean getOptionalIconsVisible() {
1082 return mOptionalIconsVisible;
1083 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001084}