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