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