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