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