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