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