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