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