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