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