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