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