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