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