MenuBuilder.java revision 07a07ce59efb770e9fb9ca53a0133e5e64a63bbc
1// Copyright 2012 Google Inc. All Rights Reserved. 2 3package android.support.v7.internal.view.menu; 4 5import android.content.ComponentName; 6import android.content.Context; 7import android.content.Intent; 8import android.content.pm.PackageManager; 9import android.content.pm.ResolveInfo; 10import android.content.res.Configuration; 11import android.content.res.Resources; 12import android.graphics.drawable.Drawable; 13import android.os.Bundle; 14import android.os.Parcelable; 15import android.support.v4.view.MenuItemCompat; 16import android.support.v7.appcompat.R; 17import android.support.v4.view.ActionProvider; 18import android.support.v4.internal.view.SupportMenu; 19import android.support.v4.internal.view.SupportMenuItem; 20import android.util.SparseArray; 21import android.view.ContextMenu; 22import android.view.KeyCharacterMap; 23import android.view.KeyEvent; 24import android.view.MenuItem; 25import android.view.SubMenu; 26import android.view.View; 27 28import java.lang.ref.WeakReference; 29import java.util.ArrayList; 30import java.util.List; 31import java.util.concurrent.CopyOnWriteArrayList; 32 33/** 34 * Implementation of the {@link android.support.v4.internal.view.SupportMenu} interface for creating a 35 * standard menu UI. 36 */ 37public class MenuBuilder implements SupportMenu { 38 39 private static final String TAG = "MenuBuilder"; 40 41 private static final String PRESENTER_KEY = "android:menu:presenters"; 42 private static final String ACTION_VIEW_STATES_KEY = "android:menu:actionviewstates"; 43 private static final String EXPANDED_ACTION_VIEW_ID = "android:menu:expandedactionview"; 44 45 private static final int[] sCategoryToOrder = new int[]{ 46 1, /* No category */ 47 4, /* CONTAINER */ 48 5, /* SYSTEM */ 49 3, /* SECONDARY */ 50 2, /* ALTERNATIVE */ 51 0, /* SELECTED_ALTERNATIVE */ 52 }; 53 54 private final Context mContext; 55 56 private final Resources mResources; 57 58 /** 59 * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode() instead of accessing 60 * this directly. 61 */ 62 private boolean mQwertyMode; 63 64 /** 65 * Whether the shortcuts should be visible on menus. Use isShortcutsVisible() instead of 66 * accessing this directly. 67 */ 68 private boolean mShortcutsVisible; 69 70 /** 71 * Callback that will receive the various menu-related events generated by this class. Use 72 * getCallback to get a reference to the callback. 73 */ 74 private Callback mCallback; 75 76 /** 77 * Contains all of the items for this menu 78 */ 79 private ArrayList<MenuItemImpl> mItems; 80 81 /** 82 * Contains only the items that are currently visible. This will be created/refreshed from 83 * {@link #getVisibleItems()} 84 */ 85 private ArrayList<MenuItemImpl> mVisibleItems; 86 87 /** 88 * Whether or not the items (or any one item's shown state) has changed since it was last 89 * fetched from {@link #getVisibleItems()} 90 */ 91 private boolean mIsVisibleItemsStale; 92 93 /** 94 * Contains only the items that should appear in the Action Bar, if present. 95 */ 96 private ArrayList<MenuItemImpl> mActionItems; 97 98 /** 99 * Contains items that should NOT appear in the Action Bar, if present. 100 */ 101 private ArrayList<MenuItemImpl> mNonActionItems; 102 103 /** 104 * Whether or not the items (or any one item's action state) has changed since it was last 105 * fetched. 106 */ 107 private boolean mIsActionItemsStale; 108 109 /** 110 * Default value for how added items should show in the action list. 111 */ 112 private int mDefaultShowAsAction = SupportMenuItem.SHOW_AS_ACTION_NEVER; 113 114 /** 115 * Current use case is Context Menus: As Views populate the context menu, each one has extra 116 * information that should be passed along. This is the current menu info that should be set on 117 * all items added to this menu. 118 */ 119 private ContextMenu.ContextMenuInfo mCurrentMenuInfo; 120 121 /** 122 * Header title for menu types that have a header (context and submenus) 123 */ 124 CharSequence mHeaderTitle; 125 126 /** 127 * Header icon for menu types that have a header and support icons (context) 128 */ 129 Drawable mHeaderIcon; 130 131 /** 132 * Header custom view for menu types that have a header and support custom views (context) 133 */ 134 View mHeaderView; 135 136 /** 137 * Prevents onItemsChanged from doing its junk, useful for batching commands that may 138 * individually call onItemsChanged. 139 */ 140 private boolean mPreventDispatchingItemsChanged = false; 141 142 private boolean mItemsChangedWhileDispatchPrevented = false; 143 144 private boolean mOptionalIconsVisible = false; 145 146 private boolean mIsClosing = false; 147 148 private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>(); 149 150 private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters = 151 new CopyOnWriteArrayList<WeakReference<MenuPresenter>>(); 152 153 /** 154 * Currently expanded menu item; must be collapsed when we clear. 155 */ 156 private MenuItemImpl mExpandedItem; 157 158 /** 159 * Called by menu to notify of close and selection changes. 160 */ 161 public interface Callback { 162 163 /** 164 * Called when a menu item is selected. 165 * 166 * @param menu The menu that is the parent of the item 167 * @param item The menu item that is selected 168 * @return whether the menu item selection was handled 169 */ 170 public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item); 171 172 /** 173 * Called when the mode of the menu changes (for example, from icon to expanded). 174 * 175 * @param menu the menu that has changed modes 176 */ 177 public void onMenuModeChange(MenuBuilder menu); 178 } 179 180 /** 181 * Called by menu items to execute their associated action 182 */ 183 public interface ItemInvoker { 184 185 public boolean invokeItem(MenuItemImpl item); 186 } 187 188 public MenuBuilder(Context context) { 189 mContext = context; 190 mResources = context.getResources(); 191 192 mItems = new ArrayList<MenuItemImpl>(); 193 194 mVisibleItems = new ArrayList<MenuItemImpl>(); 195 mIsVisibleItemsStale = true; 196 197 mActionItems = new ArrayList<MenuItemImpl>(); 198 mNonActionItems = new ArrayList<MenuItemImpl>(); 199 mIsActionItemsStale = true; 200 201 setShortcutsVisibleInner(true); 202 } 203 204 public MenuBuilder setDefaultShowAsAction(int defaultShowAsAction) { 205 mDefaultShowAsAction = defaultShowAsAction; 206 return this; 207 } 208 209 /** 210 * Add a presenter to this menu. This will only hold a WeakReference; you do not need to 211 * explicitly remove a presenter, but you can using {@link #removeMenuPresenter(MenuPresenter)}. 212 * 213 * @param presenter The presenter to add 214 */ 215 public void addMenuPresenter(MenuPresenter presenter) { 216 mPresenters.add(new WeakReference<MenuPresenter>(presenter)); 217 presenter.initForMenu(mContext, this); 218 mIsActionItemsStale = true; 219 } 220 221 /** 222 * Remove a presenter from this menu. That presenter will no longer receive notifications of 223 * updates to this menu's data. 224 * 225 * @param presenter The presenter to remove 226 */ 227 public void removeMenuPresenter(MenuPresenter presenter) { 228 for (WeakReference<MenuPresenter> ref : mPresenters) { 229 final MenuPresenter item = ref.get(); 230 if (item == null || item == presenter) { 231 mPresenters.remove(ref); 232 } 233 } 234 } 235 236 private void dispatchPresenterUpdate(boolean cleared) { 237 if (mPresenters.isEmpty()) { 238 return; 239 } 240 241 stopDispatchingItemsChanged(); 242 for (WeakReference<MenuPresenter> ref : mPresenters) { 243 final MenuPresenter presenter = ref.get(); 244 if (presenter == null) { 245 mPresenters.remove(ref); 246 } else { 247 presenter.updateMenuView(cleared); 248 } 249 } 250 startDispatchingItemsChanged(); 251 } 252 253 private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu) { 254 if (mPresenters.isEmpty()) { 255 return false; 256 } 257 258 boolean result = false; 259 260 for (WeakReference<MenuPresenter> ref : mPresenters) { 261 final MenuPresenter presenter = ref.get(); 262 if (presenter == null) { 263 mPresenters.remove(ref); 264 } else if (!result) { 265 result = presenter.onSubMenuSelected(subMenu); 266 } 267 } 268 return result; 269 } 270 271 private void dispatchSaveInstanceState(Bundle outState) { 272 if (mPresenters.isEmpty()) { 273 return; 274 } 275 276 SparseArray<Parcelable> presenterStates = new SparseArray<Parcelable>(); 277 278 for (WeakReference<MenuPresenter> ref : mPresenters) { 279 final MenuPresenter presenter = ref.get(); 280 if (presenter == null) { 281 mPresenters.remove(ref); 282 } else { 283 final int id = presenter.getId(); 284 if (id > 0) { 285 final Parcelable state = presenter.onSaveInstanceState(); 286 if (state != null) { 287 presenterStates.put(id, state); 288 } 289 } 290 } 291 } 292 293 outState.putSparseParcelableArray(PRESENTER_KEY, presenterStates); 294 } 295 296 private void dispatchRestoreInstanceState(Bundle state) { 297 SparseArray<Parcelable> presenterStates = state.getSparseParcelableArray(PRESENTER_KEY); 298 299 if (presenterStates == null || mPresenters.isEmpty()) { 300 return; 301 } 302 303 for (WeakReference<MenuPresenter> ref : mPresenters) { 304 final MenuPresenter presenter = ref.get(); 305 if (presenter == null) { 306 mPresenters.remove(ref); 307 } else { 308 final int id = presenter.getId(); 309 if (id > 0) { 310 Parcelable parcel = presenterStates.get(id); 311 if (parcel != null) { 312 presenter.onRestoreInstanceState(parcel); 313 } 314 } 315 } 316 } 317 } 318 319 public void savePresenterStates(Bundle outState) { 320 dispatchSaveInstanceState(outState); 321 } 322 323 public void restorePresenterStates(Bundle state) { 324 dispatchRestoreInstanceState(state); 325 } 326 327 public void saveActionViewStates(Bundle outStates) { 328 SparseArray<Parcelable> viewStates = null; 329 330 final int itemCount = size(); 331 for (int i = 0; i < itemCount; i++) { 332 final MenuItem item = getItem(i); 333 final View v = MenuItemCompat.getActionView(item); 334 if (v != null && v.getId() != View.NO_ID) { 335 if (viewStates == null) { 336 viewStates = new SparseArray<Parcelable>(); 337 } 338 v.saveHierarchyState(viewStates); 339 if (MenuItemCompat.isActionViewExpanded(item)) { 340 outStates.putInt(EXPANDED_ACTION_VIEW_ID, item.getItemId()); 341 } 342 } 343 if (item.hasSubMenu()) { 344 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu(); 345 subMenu.saveActionViewStates(outStates); 346 } 347 } 348 349 if (viewStates != null) { 350 outStates.putSparseParcelableArray(getActionViewStatesKey(), viewStates); 351 } 352 } 353 354 public void restoreActionViewStates(Bundle states) { 355 if (states == null) { 356 return; 357 } 358 359 SparseArray<Parcelable> viewStates = states.getSparseParcelableArray( 360 getActionViewStatesKey()); 361 362 final int itemCount = size(); 363 for (int i = 0; i < itemCount; i++) { 364 final MenuItem item = getItem(i); 365 final View v = MenuItemCompat.getActionView(item); 366 if (v != null && v.getId() != View.NO_ID) { 367 v.restoreHierarchyState(viewStates); 368 } 369 if (item.hasSubMenu()) { 370 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu(); 371 subMenu.restoreActionViewStates(states); 372 } 373 } 374 375 final int expandedId = states.getInt(EXPANDED_ACTION_VIEW_ID); 376 if (expandedId > 0) { 377 MenuItem itemToExpand = findItem(expandedId); 378 if (itemToExpand != null) { 379 MenuItemCompat.expandActionView(itemToExpand); 380 } 381 } 382 } 383 384 protected String getActionViewStatesKey() { 385 return ACTION_VIEW_STATES_KEY; 386 } 387 388 public void setCallback(Callback cb) { 389 mCallback = cb; 390 } 391 392 /** 393 * Adds an item to the menu. The other add methods funnel to this. 394 */ 395 private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) { 396 final int ordering = getOrdering(categoryOrder); 397 398 final MenuItemImpl item = new MenuItemImpl(this, group, id, categoryOrder, 399 ordering, title, mDefaultShowAsAction); 400 401 if (mCurrentMenuInfo != null) { 402 // Pass along the current menu info 403 item.setMenuInfo(mCurrentMenuInfo); 404 } 405 406 mItems.add(findInsertIndex(mItems, ordering), item); 407 onItemsChanged(true); 408 409 return item; 410 } 411 412 @Override 413 public MenuItem add(CharSequence title) { 414 return addInternal(0, 0, 0, title); 415 } 416 417 @Override 418 public MenuItem add(int titleRes) { 419 return addInternal(0, 0, 0, mResources.getString(titleRes)); 420 } 421 422 @Override 423 public MenuItem add(int group, int id, int categoryOrder, CharSequence title) { 424 return addInternal(group, id, categoryOrder, title); 425 } 426 427 @Override 428 public MenuItem add(int group, int id, int categoryOrder, int title) { 429 return addInternal(group, id, categoryOrder, mResources.getString(title)); 430 } 431 432 @Override 433 public SubMenu addSubMenu(CharSequence title) { 434 return addSubMenu(0, 0, 0, title); 435 } 436 437 @Override 438 public SubMenu addSubMenu(int titleRes) { 439 return addSubMenu(0, 0, 0, mResources.getString(titleRes)); 440 } 441 442 @Override 443 public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) { 444 final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title); 445 final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item); 446 item.setSubMenu(subMenu); 447 448 return subMenu; 449 } 450 451 @Override 452 public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) { 453 return addSubMenu(group, id, categoryOrder, mResources.getString(title)); 454 } 455 456 @Override 457 public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller, 458 Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) { 459 PackageManager pm = mContext.getPackageManager(); 460 final List<ResolveInfo> lri = 461 pm.queryIntentActivityOptions(caller, specifics, intent, 0); 462 final int N = lri != null ? lri.size() : 0; 463 464 if ((flags & FLAG_APPEND_TO_GROUP) == 0) { 465 removeGroup(group); 466 } 467 468 for (int i = 0; i < N; i++) { 469 final ResolveInfo ri = lri.get(i); 470 Intent rintent = new Intent( 471 ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]); 472 rintent.setComponent(new ComponentName( 473 ri.activityInfo.applicationInfo.packageName, 474 ri.activityInfo.name)); 475 final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm)) 476 .setIcon(ri.loadIcon(pm)) 477 .setIntent(rintent); 478 if (outSpecificItems != null && ri.specificIndex >= 0) { 479 outSpecificItems[ri.specificIndex] = item; 480 } 481 } 482 483 return N; 484 } 485 486 @Override 487 public void removeItem(int id) { 488 removeItemAtInt(findItemIndex(id), true); 489 } 490 491 @Override 492 public void removeGroup(int group) { 493 final int i = findGroupIndex(group); 494 495 if (i >= 0) { 496 final int maxRemovable = mItems.size() - i; 497 int numRemoved = 0; 498 while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) { 499 // Don't force update for each one, this method will do it at the end 500 removeItemAtInt(i, false); 501 } 502 503 // Notify menu views 504 onItemsChanged(true); 505 } 506 } 507 508 /** 509 * Remove the item at the given index and optionally forces menu views to update. 510 * 511 * @param index The index of the item to be removed. If this index is 512 * invalid an exception is thrown. 513 * @param updateChildrenOnMenuViews Whether to force update on menu views. Please make sure you 514 * eventually call this after your batch of removals. 515 */ 516 private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) { 517 if ((index < 0) || (index >= mItems.size())) { 518 return; 519 } 520 521 mItems.remove(index); 522 523 if (updateChildrenOnMenuViews) { 524 onItemsChanged(true); 525 } 526 } 527 528 public void removeItemAt(int index) { 529 removeItemAtInt(index, true); 530 } 531 532 public void clearAll() { 533 mPreventDispatchingItemsChanged = true; 534 clear(); 535 clearHeader(); 536 mPreventDispatchingItemsChanged = false; 537 mItemsChangedWhileDispatchPrevented = false; 538 onItemsChanged(true); 539 } 540 541 @Override 542 public void clear() { 543 if (mExpandedItem != null) { 544 collapseItemActionView(mExpandedItem); 545 } 546 mItems.clear(); 547 548 onItemsChanged(true); 549 } 550 551 void setExclusiveItemChecked(MenuItem item) { 552 final int group = item.getGroupId(); 553 554 final int N = mItems.size(); 555 for (int i = 0; i < N; i++) { 556 MenuItemImpl curItem = mItems.get(i); 557 if (curItem.getGroupId() == group) { 558 if (!curItem.isExclusiveCheckable()) { 559 continue; 560 } 561 if (!curItem.isCheckable()) { 562 continue; 563 } 564 565 // Check the item meant to be checked, uncheck the others (that are in the group) 566 curItem.setCheckedInt(curItem == item); 567 } 568 } 569 } 570 571 @Override 572 public void setGroupCheckable(int group, boolean checkable, boolean exclusive) { 573 final int N = mItems.size(); 574 575 for (int i = 0; i < N; i++) { 576 MenuItemImpl item = mItems.get(i); 577 if (item.getGroupId() == group) { 578 item.setExclusiveCheckable(exclusive); 579 item.setCheckable(checkable); 580 } 581 } 582 } 583 584 @Override 585 public void setGroupVisible(int group, boolean visible) { 586 final int N = mItems.size(); 587 588 // We handle the notification of items being changed ourselves, so we use setVisibleInt 589 // rather than setVisible and at the end notify of items being changed 590 591 boolean changedAtLeastOneItem = false; 592 for (int i = 0; i < N; i++) { 593 MenuItemImpl item = mItems.get(i); 594 if (item.getGroupId() == group) { 595 if (item.setVisibleInt(visible)) { 596 changedAtLeastOneItem = true; 597 } 598 } 599 } 600 601 if (changedAtLeastOneItem) { 602 onItemsChanged(true); 603 } 604 } 605 606 @Override 607 public void setGroupEnabled(int group, boolean enabled) { 608 final int N = mItems.size(); 609 610 for (int i = 0; i < N; i++) { 611 MenuItemImpl item = mItems.get(i); 612 if (item.getGroupId() == group) { 613 item.setEnabled(enabled); 614 } 615 } 616 } 617 618 @Override 619 public boolean hasVisibleItems() { 620 final int size = size(); 621 622 for (int i = 0; i < size; i++) { 623 MenuItemImpl item = mItems.get(i); 624 if (item.isVisible()) { 625 return true; 626 } 627 } 628 629 return false; 630 } 631 632 @Override 633 public MenuItem findItem(int id) { 634 final int size = size(); 635 for (int i = 0; i < size; i++) { 636 MenuItemImpl item = mItems.get(i); 637 if (item.getItemId() == id) { 638 return item; 639 } else if (item.hasSubMenu()) { 640 MenuItem possibleItem = item.getSubMenu().findItem(id); 641 642 if (possibleItem != null) { 643 return possibleItem; 644 } 645 } 646 } 647 648 return null; 649 } 650 651 public int findItemIndex(int id) { 652 final int size = size(); 653 654 for (int i = 0; i < size; i++) { 655 MenuItemImpl item = mItems.get(i); 656 if (item.getItemId() == id) { 657 return i; 658 } 659 } 660 661 return -1; 662 } 663 664 public int findGroupIndex(int group) { 665 return findGroupIndex(group, 0); 666 } 667 668 public int findGroupIndex(int group, int start) { 669 final int size = size(); 670 671 if (start < 0) { 672 start = 0; 673 } 674 675 for (int i = start; i < size; i++) { 676 final MenuItemImpl item = mItems.get(i); 677 678 if (item.getGroupId() == group) { 679 return i; 680 } 681 } 682 683 return -1; 684 } 685 686 @Override 687 public int size() { 688 return mItems.size(); 689 } 690 691 @Override 692 public MenuItem getItem(int index) { 693 return mItems.get(index); 694 } 695 696 @Override 697 public boolean isShortcutKey(int keyCode, KeyEvent event) { 698 return findItemWithShortcutForKey(keyCode, event) != null; 699 } 700 701 @Override 702 public void setQwertyMode(boolean isQwerty) { 703 mQwertyMode = isQwerty; 704 705 onItemsChanged(false); 706 } 707 708 /** 709 * Returns the ordering across all items. This will grab the category from the upper bits, find 710 * out how to order the category with respect to other categories, and combine it with the lower 711 * bits. 712 * 713 * @param categoryOrder The category order for a particular item (if it has not been or/add with 714 * a category, the default category is assumed). 715 * @return An ordering integer that can be used to order this item across all the items (even 716 * from other categories). 717 */ 718 private static int getOrdering(int categoryOrder) { 719 final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT; 720 721 if (index < 0 || index >= sCategoryToOrder.length) { 722 throw new IllegalArgumentException("order does not contain a valid category."); 723 } 724 725 return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK); 726 } 727 728 /** 729 * @return whether the menu shortcuts are in qwerty mode or not 730 */ 731 boolean isQwertyMode() { 732 return mQwertyMode; 733 } 734 735 /** 736 * Sets whether the shortcuts should be visible on menus. Devices without hardware key input 737 * will never make shortcuts visible even if this method is passed 'true'. 738 * 739 * @param shortcutsVisible Whether shortcuts should be visible (if true and a menu item does not 740 * have a shortcut defined, that item will still NOT show a shortcut) 741 */ 742 public void setShortcutsVisible(boolean shortcutsVisible) { 743 if (mShortcutsVisible == shortcutsVisible) { 744 return; 745 } 746 747 setShortcutsVisibleInner(shortcutsVisible); 748 onItemsChanged(false); 749 } 750 751 private void setShortcutsVisibleInner(boolean shortcutsVisible) { 752 mShortcutsVisible = shortcutsVisible 753 && mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS 754 && mResources.getBoolean(R.bool.abc_config_showMenuShortcutsWhenKeyboardPresent); 755 } 756 757 /** 758 * @return Whether shortcuts should be visible on menus. 759 */ 760 public boolean isShortcutsVisible() { 761 return mShortcutsVisible; 762 } 763 764 Resources getResources() { 765 return mResources; 766 } 767 768 public Context getContext() { 769 return mContext; 770 } 771 772 boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) { 773 return mCallback != null && mCallback.onMenuItemSelected(menu, item); 774 } 775 776 /** 777 * Dispatch a mode change event to this menu's callback. 778 */ 779 public void changeMenuMode() { 780 if (mCallback != null) { 781 mCallback.onMenuModeChange(this); 782 } 783 } 784 785 private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) { 786 for (int i = items.size() - 1; i >= 0; i--) { 787 MenuItemImpl item = items.get(i); 788 if (item.getOrdering() <= ordering) { 789 return i + 1; 790 } 791 } 792 793 return 0; 794 } 795 796 @Override 797 public boolean performShortcut(int keyCode, KeyEvent event, int flags) { 798 final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event); 799 800 boolean handled = false; 801 802 if (item != null) { 803 handled = performItemAction(item, flags); 804 } 805 806 if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) { 807 close(true); 808 } 809 810 return handled; 811 } 812 813 /* 814 * This function will return all the menu and sub-menu items that can 815 * be directly (the shortcut directly corresponds) and indirectly 816 * (the ALT-enabled char corresponds to the shortcut) associated 817 * with the keyCode. 818 */ 819 @SuppressWarnings("deprecation") 820 void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) { 821 final boolean qwerty = isQwertyMode(); 822 final int metaState = event.getMetaState(); 823 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); 824 // Get the chars associated with the keyCode (i.e using any chording combo) 825 final boolean isKeyCodeMapped = event.getKeyData(possibleChars); 826 // The delete key is not mapped to '\b' so we treat it specially 827 if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) { 828 return; 829 } 830 831 // Look for an item whose shortcut is this key. 832 final int N = mItems.size(); 833 for (int i = 0; i < N; i++) { 834 MenuItemImpl item = mItems.get(i); 835 if (item.hasSubMenu()) { 836 ((MenuBuilder) item.getSubMenu()) 837 .findItemsWithShortcutForKey(items, keyCode, event); 838 } 839 final char shortcutChar = qwerty ? item.getAlphabeticShortcut() 840 : item.getNumericShortcut(); 841 if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) && 842 (shortcutChar != 0) && 843 (shortcutChar == possibleChars.meta[0] 844 || shortcutChar == possibleChars.meta[2] 845 || (qwerty && shortcutChar == '\b' && 846 keyCode == KeyEvent.KEYCODE_DEL)) && 847 item.isEnabled()) { 848 items.add(item); 849 } 850 } 851 } 852 853 /* 854 * We want to return the menu item associated with the key, but if there is no 855 * ambiguity (i.e. there is only one menu item corresponding to the key) we want 856 * to return it even if it's not an exact match; this allow the user to 857 * _not_ use the ALT key for example, making the use of shortcuts slightly more 858 * user-friendly. An example is on the G1, '!' and '1' are on the same key, and 859 * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut). 860 * 861 * On the other hand, if two (or more) shortcuts corresponds to the same key, 862 * we have to only return the exact match. 863 */ 864 @SuppressWarnings("deprecation") 865 MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) { 866 // Get all items that can be associated directly or indirectly with the keyCode 867 ArrayList<MenuItemImpl> items = mTempShortcutItemList; 868 items.clear(); 869 findItemsWithShortcutForKey(items, keyCode, event); 870 871 if (items.isEmpty()) { 872 return null; 873 } 874 875 final int metaState = event.getMetaState(); 876 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); 877 // Get the chars associated with the keyCode (i.e using any chording combo) 878 event.getKeyData(possibleChars); 879 880 // If we have only one element, we can safely returns it 881 final int size = items.size(); 882 if (size == 1) { 883 return items.get(0); 884 } 885 886 final boolean qwerty = isQwertyMode(); 887 // If we found more than one item associated with the key, 888 // we have to return the exact match 889 for (int i = 0; i < size; i++) { 890 final MenuItemImpl item = items.get(i); 891 final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : 892 item.getNumericShortcut(); 893 if ((shortcutChar == possibleChars.meta[0] && 894 (metaState & KeyEvent.META_ALT_ON) == 0) 895 || (shortcutChar == possibleChars.meta[2] && 896 (metaState & KeyEvent.META_ALT_ON) != 0) 897 || (qwerty && shortcutChar == '\b' && 898 keyCode == KeyEvent.KEYCODE_DEL)) { 899 return item; 900 } 901 } 902 return null; 903 } 904 905 @Override 906 public boolean performIdentifierAction(int id, int flags) { 907 // Look for an item whose identifier is the id. 908 return performItemAction(findItem(id), flags); 909 } 910 911 public boolean performItemAction(MenuItem item, int flags) { 912 MenuItemImpl itemImpl = (MenuItemImpl) item; 913 914 if (itemImpl == null || !itemImpl.isEnabled()) { 915 return false; 916 } 917 918 boolean invoked = itemImpl.invoke(); 919 920 final ActionProvider provider = itemImpl.getSupportActionProvider(); 921 final boolean providerHasSubMenu = provider != null && provider.hasSubMenu(); 922 if (itemImpl.hasCollapsibleActionView()) { 923 invoked |= itemImpl.expandActionView(); 924 if (invoked) { 925 close(true); 926 } 927 } else if (itemImpl.hasSubMenu() || providerHasSubMenu) { 928 close(false); 929 930 if (!itemImpl.hasSubMenu()) { 931 itemImpl.setSubMenu(new SubMenuBuilder(getContext(), this, itemImpl)); 932 } 933 934 final SubMenuBuilder subMenu = (SubMenuBuilder) itemImpl.getSubMenu(); 935 if (providerHasSubMenu) { 936 provider.onPrepareSubMenu(subMenu); 937 } 938 invoked |= dispatchSubMenuSelected(subMenu); 939 if (!invoked) { 940 close(true); 941 } 942 } else { 943 if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) { 944 close(true); 945 } 946 } 947 948 return invoked; 949 } 950 951 /** 952 * Closes the visible menu. 953 * 954 * @param allMenusAreClosing Whether the menus are completely closing (true), or whether there 955 * is another menu coming in this menu's place (false). For example, 956 * if the menu is closing because a sub menu is about to be shown, 957 * <var>allMenusAreClosing</var> is false. 958 */ 959 final void close(boolean allMenusAreClosing) { 960 if (mIsClosing) { 961 return; 962 } 963 964 mIsClosing = true; 965 for (WeakReference<MenuPresenter> ref : mPresenters) { 966 final MenuPresenter presenter = ref.get(); 967 if (presenter == null) { 968 mPresenters.remove(ref); 969 } else { 970 presenter.onCloseMenu(this, allMenusAreClosing); 971 } 972 } 973 mIsClosing = false; 974 } 975 976 @Override 977 public void close() { 978 close(true); 979 } 980 981 /** 982 * Called when an item is added or removed. 983 * 984 * @param structureChanged true if the menu structure changed, false if only item properties 985 * changed. (Visibility is a structural property since it affects 986 * layout.) 987 */ 988 void onItemsChanged(boolean structureChanged) { 989 if (!mPreventDispatchingItemsChanged) { 990 if (structureChanged) { 991 mIsVisibleItemsStale = true; 992 mIsActionItemsStale = true; 993 } 994 995 dispatchPresenterUpdate(structureChanged); 996 } else { 997 mItemsChangedWhileDispatchPrevented = true; 998 } 999 } 1000 1001 /** 1002 * Stop dispatching item changed events to presenters until {@link 1003 * #startDispatchingItemsChanged()} is called. Useful when many menu operations are going to be 1004 * performed as a batch. 1005 */ 1006 public void stopDispatchingItemsChanged() { 1007 if (!mPreventDispatchingItemsChanged) { 1008 mPreventDispatchingItemsChanged = true; 1009 mItemsChangedWhileDispatchPrevented = false; 1010 } 1011 } 1012 1013 public void startDispatchingItemsChanged() { 1014 mPreventDispatchingItemsChanged = false; 1015 1016 if (mItemsChangedWhileDispatchPrevented) { 1017 mItemsChangedWhileDispatchPrevented = false; 1018 onItemsChanged(true); 1019 } 1020 } 1021 1022 /** 1023 * Called by {@link MenuItemImpl} when its visible flag is changed. 1024 * 1025 * @param item The item that has gone through a visibility change. 1026 */ 1027 void onItemVisibleChanged(MenuItemImpl item) { 1028 // Notify of items being changed 1029 mIsVisibleItemsStale = true; 1030 onItemsChanged(true); 1031 } 1032 1033 /** 1034 * Called by {@link MenuItemImpl} when its action request status is changed. 1035 * 1036 * @param item The item that has gone through a change in action request status. 1037 */ 1038 void onItemActionRequestChanged(MenuItemImpl item) { 1039 // Notify of items being changed 1040 mIsActionItemsStale = true; 1041 onItemsChanged(true); 1042 } 1043 1044 ArrayList<MenuItemImpl> getVisibleItems() { 1045 if (!mIsVisibleItemsStale) { 1046 return mVisibleItems; 1047 } 1048 1049 // Refresh the visible items 1050 mVisibleItems.clear(); 1051 1052 final int itemsSize = mItems.size(); 1053 MenuItemImpl item; 1054 for (int i = 0; i < itemsSize; i++) { 1055 item = mItems.get(i); 1056 if (item.isVisible()) { 1057 mVisibleItems.add(item); 1058 } 1059 } 1060 1061 mIsVisibleItemsStale = false; 1062 mIsActionItemsStale = true; 1063 1064 return mVisibleItems; 1065 } 1066 1067 /** 1068 * This method determines which menu items get to be 'action items' that will appear 1069 * in an action bar and which items should be 'overflow items' in a secondary menu. 1070 * The rules are as follows: 1071 * 1072 * <p>Items are considered for inclusion in the order specified within the menu. 1073 * There is a limit of mMaxActionItems as a total count, optionally including the overflow 1074 * menu button itself. This is a soft limit; if an item shares a group ID with an item 1075 * previously included as an action item, the new item will stay with its group and become 1076 * an action item itself even if it breaks the max item count limit. This is done to 1077 * limit the conceptual complexity of the items presented within an action bar. Only a few 1078 * unrelated concepts should be presented to the user in this space, and groups are treated 1079 * as a single concept. 1080 * 1081 * <p>There is also a hard limit of consumed measurable space: mActionWidthLimit. This 1082 * limit may be broken by a single item that exceeds the remaining space, but no further 1083 * items may be added. If an item that is part of a group cannot fit within the remaining 1084 * measured width, the entire group will be demoted to overflow. This is done to ensure room 1085 * for navigation and other affordances in the action bar as well as reduce general UI clutter. 1086 * 1087 * <p>The space freed by demoting a full group cannot be consumed by future menu items. 1088 * Once items begin to overflow, all future items become overflow items as well. This is 1089 * to avoid inadvertent reordering that may break the app's intended design. 1090 */ 1091 public void flagActionItems() { 1092 if (!mIsActionItemsStale) { 1093 return; 1094 } 1095 1096 // Presenters flag action items as needed. 1097 boolean flagged = false; 1098 for (WeakReference<MenuPresenter> ref : mPresenters) { 1099 final MenuPresenter presenter = ref.get(); 1100 if (presenter == null) { 1101 mPresenters.remove(ref); 1102 } else { 1103 flagged |= presenter.flagActionItems(); 1104 } 1105 } 1106 1107 if (flagged) { 1108 mActionItems.clear(); 1109 mNonActionItems.clear(); 1110 ArrayList<MenuItemImpl> visibleItems = getVisibleItems(); 1111 final int itemsSize = visibleItems.size(); 1112 for (int i = 0; i < itemsSize; i++) { 1113 MenuItemImpl item = visibleItems.get(i); 1114 if (item.isActionButton()) { 1115 mActionItems.add(item); 1116 } else { 1117 mNonActionItems.add(item); 1118 } 1119 } 1120 } else { 1121 // Nobody flagged anything, everything is a non-action item. 1122 // (This happens during a first pass with no action-item presenters.) 1123 mActionItems.clear(); 1124 mNonActionItems.clear(); 1125 mNonActionItems.addAll(getVisibleItems()); 1126 } 1127 mIsActionItemsStale = false; 1128 } 1129 1130 ArrayList<MenuItemImpl> getActionItems() { 1131 flagActionItems(); 1132 return mActionItems; 1133 } 1134 1135 ArrayList<MenuItemImpl> getNonActionItems() { 1136 flagActionItems(); 1137 return mNonActionItems; 1138 } 1139 1140 public void clearHeader() { 1141 mHeaderIcon = null; 1142 mHeaderTitle = null; 1143 mHeaderView = null; 1144 1145 onItemsChanged(false); 1146 } 1147 1148 private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes, 1149 final Drawable icon, final View view) { 1150 final Resources r = getResources(); 1151 1152 if (view != null) { 1153 mHeaderView = view; 1154 1155 // If using a custom view, then the title and icon aren't used 1156 mHeaderTitle = null; 1157 mHeaderIcon = null; 1158 } else { 1159 if (titleRes > 0) { 1160 mHeaderTitle = r.getText(titleRes); 1161 } else if (title != null) { 1162 mHeaderTitle = title; 1163 } 1164 1165 if (iconRes > 0) { 1166 mHeaderIcon = r.getDrawable(iconRes); 1167 } else if (icon != null) { 1168 mHeaderIcon = icon; 1169 } 1170 1171 // If using the title or icon, then a custom view isn't used 1172 mHeaderView = null; 1173 } 1174 1175 // Notify of change 1176 onItemsChanged(false); 1177 } 1178 1179 /** 1180 * Sets the header's title. This replaces the header view. Called by the builder-style methods 1181 * of subclasses. 1182 * 1183 * @param title The new title. 1184 * @return This MenuBuilder so additional setters can be called. 1185 */ 1186 protected MenuBuilder setHeaderTitleInt(CharSequence title) { 1187 setHeaderInternal(0, title, 0, null, null); 1188 return this; 1189 } 1190 1191 /** 1192 * Sets the header's title. This replaces the header view. Called by the builder-style methods 1193 * of subclasses. 1194 * 1195 * @param titleRes The new title (as a resource ID). 1196 * @return This MenuBuilder so additional setters can be called. 1197 */ 1198 protected MenuBuilder setHeaderTitleInt(int titleRes) { 1199 setHeaderInternal(titleRes, null, 0, null, null); 1200 return this; 1201 } 1202 1203 /** 1204 * Sets the header's icon. This replaces the header view. Called by the builder-style methods of 1205 * subclasses. 1206 * 1207 * @param icon The new icon. 1208 * @return This MenuBuilder so additional setters can be called. 1209 */ 1210 protected MenuBuilder setHeaderIconInt(Drawable icon) { 1211 setHeaderInternal(0, null, 0, icon, null); 1212 return this; 1213 } 1214 1215 /** 1216 * Sets the header's icon. This replaces the header view. Called by the builder-style methods of 1217 * subclasses. 1218 * 1219 * @param iconRes The new icon (as a resource ID). 1220 * @return This MenuBuilder so additional setters can be called. 1221 */ 1222 protected MenuBuilder setHeaderIconInt(int iconRes) { 1223 setHeaderInternal(0, null, iconRes, null, null); 1224 return this; 1225 } 1226 1227 /** 1228 * Sets the header's view. This replaces the title and icon. Called by the builder-style methods 1229 * of subclasses. 1230 * 1231 * @param view The new view. 1232 * @return This MenuBuilder so additional setters can be called. 1233 */ 1234 protected MenuBuilder setHeaderViewInt(View view) { 1235 setHeaderInternal(0, null, 0, null, view); 1236 return this; 1237 } 1238 1239 public CharSequence getHeaderTitle() { 1240 return mHeaderTitle; 1241 } 1242 1243 public Drawable getHeaderIcon() { 1244 return mHeaderIcon; 1245 } 1246 1247 public View getHeaderView() { 1248 return mHeaderView; 1249 } 1250 1251 /** 1252 * Gets the root menu (if this is a submenu, find its root menu). 1253 * 1254 * @return The root menu. 1255 */ 1256 public MenuBuilder getRootMenu() { 1257 return this; 1258 } 1259 1260 /** 1261 * Sets the current menu info that is set on all items added to this menu (until this is called 1262 * again with different menu info, in which case that one will be added to all subsequent item 1263 * additions). 1264 * 1265 * @param menuInfo The extra menu information to add. 1266 */ 1267 public void setCurrentMenuInfo(ContextMenu.ContextMenuInfo menuInfo) { 1268 mCurrentMenuInfo = menuInfo; 1269 } 1270 1271 void setOptionalIconsVisible(boolean visible) { 1272 mOptionalIconsVisible = visible; 1273 } 1274 1275 boolean getOptionalIconsVisible() { 1276 return mOptionalIconsVisible; 1277 } 1278 1279 public boolean expandItemActionView(MenuItemImpl item) { 1280 if (mPresenters.isEmpty()) { 1281 return false; 1282 } 1283 1284 boolean expanded = false; 1285 1286 stopDispatchingItemsChanged(); 1287 for (WeakReference<MenuPresenter> ref : mPresenters) { 1288 final MenuPresenter presenter = ref.get(); 1289 if (presenter == null) { 1290 mPresenters.remove(ref); 1291 } else if ((expanded = presenter.expandItemActionView(this, item))) { 1292 break; 1293 } 1294 } 1295 startDispatchingItemsChanged(); 1296 1297 if (expanded) { 1298 mExpandedItem = item; 1299 } 1300 return expanded; 1301 } 1302 1303 public boolean collapseItemActionView(MenuItemImpl item) { 1304 if (mPresenters.isEmpty() || mExpandedItem != item) { 1305 return false; 1306 } 1307 1308 boolean collapsed = false; 1309 1310 stopDispatchingItemsChanged(); 1311 for (WeakReference<MenuPresenter> ref : mPresenters) { 1312 final MenuPresenter presenter = ref.get(); 1313 if (presenter == null) { 1314 mPresenters.remove(ref); 1315 } else if ((collapsed = presenter.collapseItemActionView(this, item))) { 1316 break; 1317 } 1318 } 1319 startDispatchingItemsChanged(); 1320 1321 if (collapsed) { 1322 mExpandedItem = null; 1323 } 1324 return collapsed; 1325 } 1326 1327 public MenuItemImpl getExpandedItem() { 1328 return mExpandedItem; 1329 } 1330} 1331 1332