MenuBuilder.java revision f013e1afd1e68af5e3b868c26a653bbfb39538f8
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.Resources; 26import android.graphics.drawable.Drawable; 27import android.os.Bundle; 28import android.os.Parcelable; 29import android.util.SparseArray; 30import android.view.ContextThemeWrapper; 31import android.view.KeyCharacterMap; 32import android.view.KeyEvent; 33import android.view.Menu; 34import android.view.MenuItem; 35import android.view.SubMenu; 36import android.view.View; 37import android.view.ViewGroup; 38import android.view.LayoutInflater; 39import android.view.ContextMenu.ContextMenuInfo; 40import android.widget.AdapterView; 41import android.widget.BaseAdapter; 42 43import java.lang.ref.WeakReference; 44import java.util.ArrayList; 45import java.util.List; 46 47/** 48 * Implementation of the {@link android.view.Menu} interface for creating a 49 * standard menu UI. 50 */ 51public class MenuBuilder implements Menu { 52 private static final String LOGTAG = "MenuBuilder"; 53 54 /** The number of different menu types */ 55 public static final int NUM_TYPES = 3; 56 /** The menu type that represents the icon menu view */ 57 public static final int TYPE_ICON = 0; 58 /** The menu type that represents the expanded menu view */ 59 public static final int TYPE_EXPANDED = 1; 60 /** 61 * The menu type that represents a menu dialog. Examples are context and sub 62 * menus. This menu type will not have a corresponding MenuView, but it will 63 * have an ItemView. 64 */ 65 public static final int TYPE_DIALOG = 2; 66 67 private static final String VIEWS_TAG = "android:views"; 68 69 // Order must be the same order as the TYPE_* 70 static final int THEME_RES_FOR_TYPE[] = new int[] { 71 com.android.internal.R.style.Theme_IconMenu, 72 com.android.internal.R.style.Theme_ExpandedMenu, 73 0, 74 }; 75 76 // Order must be the same order as the TYPE_* 77 static final int LAYOUT_RES_FOR_TYPE[] = new int[] { 78 com.android.internal.R.layout.icon_menu_layout, 79 com.android.internal.R.layout.expanded_menu_layout, 80 0, 81 }; 82 83 // Order must be the same order as the TYPE_* 84 static final int ITEM_LAYOUT_RES_FOR_TYPE[] = new int[] { 85 com.android.internal.R.layout.icon_menu_item_layout, 86 com.android.internal.R.layout.list_menu_item_layout, 87 com.android.internal.R.layout.list_menu_item_layout, 88 }; 89 90 private static final int[] sCategoryToOrder = new int[] { 91 1, /* No category */ 92 4, /* CONTAINER */ 93 5, /* SYSTEM */ 94 3, /* SECONDARY */ 95 2, /* ALTERNATIVE */ 96 0, /* SELECTED_ALTERNATIVE */ 97 }; 98 99 private final Context mContext; 100 private final Resources mResources; 101 102 /** 103 * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode() 104 * instead of accessing this directly. 105 */ 106 private boolean mQwertyMode; 107 108 /** 109 * Whether the shortcuts should be visible on menus. Use isShortcutsVisible() 110 * instead of accessing this directly. 111 */ 112 private boolean mShortcutsVisible; 113 114 /** 115 * Callback that will receive the various menu-related events generated by 116 * this class. Use getCallback to get a reference to the callback. 117 */ 118 private Callback mCallback; 119 120 /** Contains all of the items for this menu */ 121 private ArrayList<MenuItemImpl> mItems; 122 123 /** Contains only the items that are currently visible. This will be created/refreshed from 124 * {@link #getVisibleItems()} */ 125 private ArrayList<MenuItemImpl> mVisibleItems; 126 /** 127 * Whether or not the items (or any one item's shown state) has changed since it was last 128 * fetched from {@link #getVisibleItems()} 129 */ 130 private boolean mIsVisibleItemsStale; 131 132 /** 133 * Current use case is Context Menus: As Views populate the context menu, each one has 134 * extra information that should be passed along. This is the current menu info that 135 * should be set on all items added to this menu. 136 */ 137 private ContextMenuInfo mCurrentMenuInfo; 138 139 /** Header title for menu types that have a header (context and submenus) */ 140 CharSequence mHeaderTitle; 141 /** Header icon for menu types that have a header and support icons (context) */ 142 Drawable mHeaderIcon; 143 /** Header custom view for menu types that have a header and support custom views (context) */ 144 View mHeaderView; 145 146 /** 147 * Contains the state of the View hierarchy for all menu views when the menu 148 * was frozen. 149 */ 150 private SparseArray<Parcelable> mFrozenViewStates; 151 152 /** 153 * Prevents onItemsChanged from doing its junk, useful for batching commands 154 * that may individually call onItemsChanged. 155 */ 156 private boolean mPreventDispatchingItemsChanged = false; 157 158 private boolean mOptionalIconsVisible = false; 159 160 private MenuType[] mMenuTypes; 161 class MenuType { 162 private int mMenuType; 163 164 /** The layout inflater that uses the menu type's theme */ 165 private LayoutInflater mInflater; 166 167 /** The lazily loaded {@link MenuView} */ 168 private WeakReference<MenuView> mMenuView; 169 170 MenuType(int menuType) { 171 mMenuType = menuType; 172 } 173 174 LayoutInflater getInflater() { 175 // Create an inflater that uses the given theme for the Views it inflates 176 if (mInflater == null) { 177 Context wrappedContext = new ContextThemeWrapper(mContext, 178 THEME_RES_FOR_TYPE[mMenuType]); 179 mInflater = (LayoutInflater) wrappedContext 180 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 181 } 182 183 return mInflater; 184 } 185 186 MenuView getMenuView(ViewGroup parent) { 187 if (LAYOUT_RES_FOR_TYPE[mMenuType] == 0) { 188 return null; 189 } 190 191 synchronized (this) { 192 MenuView menuView = mMenuView != null ? mMenuView.get() : null; 193 194 if (menuView == null) { 195 menuView = (MenuView) getInflater().inflate( 196 LAYOUT_RES_FOR_TYPE[mMenuType], parent, false); 197 menuView.initialize(MenuBuilder.this, mMenuType); 198 199 // Cache the view 200 mMenuView = new WeakReference<MenuView>(menuView); 201 202 if (mFrozenViewStates != null) { 203 View view = (View) menuView; 204 view.restoreHierarchyState(mFrozenViewStates); 205 206 // Clear this menu type's frozen state, since we just restored it 207 mFrozenViewStates.remove(view.getId()); 208 } 209 } 210 211 return menuView; 212 } 213 } 214 215 boolean hasMenuView() { 216 return mMenuView != null && mMenuView.get() != null; 217 } 218 } 219 220 /** 221 * Called by menu to notify of close and selection changes 222 */ 223 public interface Callback { 224 /** 225 * Called when a menu item is selected. 226 * @param menu The menu that is the parent of the item 227 * @param item The menu item that is selected 228 * @return whether the menu item selection was handled 229 */ 230 public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item); 231 232 /** 233 * Called when a menu is closed. 234 * @param menu The menu that was closed. 235 * @param allMenusAreClosing Whether the menus are completely closing (true), 236 * or whether there is another menu opening shortly 237 * (false). For example, if the menu is closing because a 238 * sub menu is about to be shown, <var>allMenusAreClosing</var> 239 * is false. 240 */ 241 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing); 242 243 /** 244 * Called when a sub menu is selected. This is a cue to open the given sub menu's decor. 245 * @param subMenu the sub menu that is being opened 246 * @return whether the sub menu selection was handled by the callback 247 */ 248 public boolean onSubMenuSelected(SubMenuBuilder subMenu); 249 250 /** 251 * Called when a sub menu is closed 252 * @param menu the sub menu that was closed 253 */ 254 public void onCloseSubMenu(SubMenuBuilder menu); 255 256 /** 257 * Called when the mode of the menu changes (for example, from icon to expanded). 258 * 259 * @param menu the menu that has changed modes 260 */ 261 public void onMenuModeChange(MenuBuilder menu); 262 } 263 264 /** 265 * Called by menu items to execute their associated action 266 */ 267 public interface ItemInvoker { 268 public boolean invokeItem(MenuItemImpl item); 269 } 270 271 public MenuBuilder(Context context) { 272 mMenuTypes = new MenuType[NUM_TYPES]; 273 274 mContext = context; 275 mResources = context.getResources(); 276 277 mItems = new ArrayList<MenuItemImpl>(); 278 279 mVisibleItems = new ArrayList<MenuItemImpl>(); 280 mIsVisibleItemsStale = true; 281 282 mShortcutsVisible = true; 283 } 284 285 public void setCallback(Callback callback) { 286 mCallback = callback; 287 } 288 289 MenuType getMenuType(int menuType) { 290 if (mMenuTypes[menuType] == null) { 291 mMenuTypes[menuType] = new MenuType(menuType); 292 } 293 294 return mMenuTypes[menuType]; 295 } 296 297 /** 298 * Gets a menu View that contains this menu's items. 299 * 300 * @param menuType The type of menu to get a View for (must be one of 301 * {@link #TYPE_ICON}, {@link #TYPE_EXPANDED}, 302 * {@link #TYPE_DIALOG}). 303 * @param parent The ViewGroup that provides a set of LayoutParams values 304 * for this menu view 305 * @return A View for the menu of type <var>menuType</var> 306 */ 307 public View getMenuView(int menuType, ViewGroup parent) { 308 // The expanded menu depends on the number if items shown in the icon menu (which 309 // is adjustable as setters/XML attributes on IconMenuView [imagine a larger LCD 310 // wanting to show more icons]). If, for example, the activity goes through 311 // an orientation change while the expanded menu is open, the icon menu's view 312 // won't have an instance anymore; so here we make sure we have an icon menu view (matching 313 // the same parent so the layout parameters from the XML are used). This 314 // will create the icon menu view and cache it (if it doesn't already exist). 315 if (menuType == TYPE_EXPANDED 316 && (mMenuTypes[TYPE_ICON] == null || !mMenuTypes[TYPE_ICON].hasMenuView())) { 317 getMenuType(TYPE_ICON).getMenuView(parent); 318 } 319 320 return (View) getMenuType(menuType).getMenuView(parent); 321 } 322 323 private int getNumIconMenuItemsShown() { 324 ViewGroup parent = null; 325 326 if (!mMenuTypes[TYPE_ICON].hasMenuView()) { 327 /* 328 * There isn't an icon menu view instantiated, so when we get it 329 * below, it will lazily instantiate it. We should pass a proper 330 * parent so it uses the layout_ attributes present in the XML 331 * layout file. 332 */ 333 if (mMenuTypes[TYPE_EXPANDED].hasMenuView()) { 334 View expandedMenuView = (View) mMenuTypes[TYPE_EXPANDED].getMenuView(null); 335 parent = (ViewGroup) expandedMenuView.getParent(); 336 } 337 } 338 339 return ((IconMenuView) getMenuView(TYPE_ICON, parent)).getNumActualItemsShown(); 340 } 341 342 /** 343 * Clears the cached menu views. Call this if the menu views need to another 344 * layout (for example, if the screen size has changed). 345 */ 346 public void clearMenuViews() { 347 for (int i = NUM_TYPES - 1; i >= 0; i--) { 348 if (mMenuTypes[i] != null) { 349 mMenuTypes[i].mMenuView = null; 350 } 351 } 352 353 for (int i = mItems.size() - 1; i >= 0; i--) { 354 MenuItemImpl item = mItems.get(i); 355 if (item.hasSubMenu()) { 356 ((SubMenuBuilder) item.getSubMenu()).clearMenuViews(); 357 } 358 item.clearItemViews(); 359 } 360 } 361 362 /** 363 * Adds an item to the menu. The other add methods funnel to this. 364 */ 365 private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) { 366 final int ordering = getOrdering(categoryOrder); 367 368 final MenuItemImpl item = new MenuItemImpl(this, group, id, categoryOrder, ordering, title); 369 370 if (mCurrentMenuInfo != null) { 371 // Pass along the current menu info 372 item.setMenuInfo(mCurrentMenuInfo); 373 } 374 375 mItems.add(findInsertIndex(mItems, ordering), item); 376 onItemsChanged(false); 377 378 return item; 379 } 380 381 public MenuItem add(CharSequence title) { 382 return addInternal(0, 0, 0, title); 383 } 384 385 public MenuItem add(int titleRes) { 386 return addInternal(0, 0, 0, mResources.getString(titleRes)); 387 } 388 389 public MenuItem add(int group, int id, int categoryOrder, CharSequence title) { 390 return addInternal(group, id, categoryOrder, title); 391 } 392 393 public MenuItem add(int group, int id, int categoryOrder, int title) { 394 return addInternal(group, id, categoryOrder, mResources.getString(title)); 395 } 396 397 public SubMenu addSubMenu(CharSequence title) { 398 return addSubMenu(0, 0, 0, title); 399 } 400 401 public SubMenu addSubMenu(int titleRes) { 402 return addSubMenu(0, 0, 0, mResources.getString(titleRes)); 403 } 404 405 public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) { 406 final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title); 407 final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item); 408 item.setSubMenu(subMenu); 409 410 return subMenu; 411 } 412 413 public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) { 414 return addSubMenu(group, id, categoryOrder, mResources.getString(title)); 415 } 416 417 public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller, 418 Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) { 419 PackageManager pm = mContext.getPackageManager(); 420 final List<ResolveInfo> lri = 421 pm.queryIntentActivityOptions(caller, specifics, intent, 0); 422 final int N = lri != null ? lri.size() : 0; 423 424 if ((flags & FLAG_APPEND_TO_GROUP) == 0) { 425 removeGroup(group); 426 } 427 428 for (int i=0; i<N; i++) { 429 final ResolveInfo ri = lri.get(i); 430 Intent rintent = new Intent( 431 ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]); 432 rintent.setComponent(new ComponentName( 433 ri.activityInfo.applicationInfo.packageName, 434 ri.activityInfo.name)); 435 final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm)) 436 .setIcon(ri.loadIcon(pm)) 437 .setIntent(rintent); 438 if (outSpecificItems != null && ri.specificIndex >= 0) { 439 outSpecificItems[ri.specificIndex] = item; 440 } 441 } 442 443 return N; 444 } 445 446 public void removeItem(int id) { 447 removeItemAtInt(findItemIndex(id), true); 448 } 449 450 public void removeGroup(int group) { 451 final int i = findGroupIndex(group); 452 453 if (i >= 0) { 454 final int maxRemovable = mItems.size() - i; 455 int numRemoved = 0; 456 while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) { 457 // Don't force update for each one, this method will do it at the end 458 removeItemAtInt(i, false); 459 } 460 461 // Notify menu views 462 onItemsChanged(false); 463 } 464 } 465 466 /** 467 * Remove the item at the given index and optionally forces menu views to 468 * update. 469 * 470 * @param index The index of the item to be removed. If this index is 471 * invalid an exception is thrown. 472 * @param updateChildrenOnMenuViews Whether to force update on menu views. 473 * Please make sure you eventually call this after your batch of 474 * removals. 475 */ 476 private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) { 477 if ((index < 0) || (index >= mItems.size())) return; 478 479 mItems.remove(index); 480 481 if (updateChildrenOnMenuViews) onItemsChanged(false); 482 } 483 484 public void removeItemAt(int index) { 485 removeItemAtInt(index, true); 486 } 487 488 public void clearAll() { 489 mPreventDispatchingItemsChanged = true; 490 clear(); 491 clearHeader(); 492 mPreventDispatchingItemsChanged = false; 493 onItemsChanged(true); 494 } 495 496 public void clear() { 497 mItems.clear(); 498 499 onItemsChanged(true); 500 } 501 502 void setExclusiveItemChecked(MenuItem item) { 503 final int group = item.getGroupId(); 504 505 final int N = mItems.size(); 506 for (int i = 0; i < N; i++) { 507 MenuItemImpl curItem = mItems.get(i); 508 if (curItem.getGroupId() == group) { 509 if (!curItem.isExclusiveCheckable()) continue; 510 if (!curItem.isCheckable()) continue; 511 512 // Check the item meant to be checked, uncheck the others (that are in the group) 513 curItem.setCheckedInt(curItem == item); 514 } 515 } 516 } 517 518 public void setGroupCheckable(int group, boolean checkable, boolean exclusive) { 519 final int N = mItems.size(); 520 521 for (int i = 0; i < N; i++) { 522 MenuItemImpl item = mItems.get(i); 523 if (item.getGroupId() == group) { 524 item.setExclusiveCheckable(exclusive); 525 item.setCheckable(checkable); 526 } 527 } 528 } 529 530 public void setGroupVisible(int group, boolean visible) { 531 final int N = mItems.size(); 532 533 // We handle the notification of items being changed ourselves, so we use setVisibleInt rather 534 // than setVisible and at the end notify of items being changed 535 536 boolean changedAtLeastOneItem = false; 537 for (int i = 0; i < N; i++) { 538 MenuItemImpl item = mItems.get(i); 539 if (item.getGroupId() == group) { 540 if (item.setVisibleInt(visible)) changedAtLeastOneItem = true; 541 } 542 } 543 544 if (changedAtLeastOneItem) onItemsChanged(false); 545 } 546 547 public void setGroupEnabled(int group, boolean enabled) { 548 final int N = mItems.size(); 549 550 for (int i = 0; i < N; i++) { 551 MenuItemImpl item = mItems.get(i); 552 if (item.getGroupId() == group) { 553 item.setEnabled(enabled); 554 } 555 } 556 } 557 558 public boolean hasVisibleItems() { 559 final int size = size(); 560 561 for (int i = 0; i < size; i++) { 562 MenuItemImpl item = mItems.get(i); 563 if (item.isVisible()) { 564 return true; 565 } 566 } 567 568 return false; 569 } 570 571 public MenuItem findItem(int id) { 572 final int size = size(); 573 for (int i = 0; i < size; i++) { 574 MenuItemImpl item = mItems.get(i); 575 if (item.getItemId() == id) { 576 return item; 577 } else if (item.hasSubMenu()) { 578 MenuItem possibleItem = item.getSubMenu().findItem(id); 579 580 if (possibleItem != null) { 581 return possibleItem; 582 } 583 } 584 } 585 586 return null; 587 } 588 589 public int findItemIndex(int id) { 590 final int size = size(); 591 592 for (int i = 0; i < size; i++) { 593 MenuItemImpl item = mItems.get(i); 594 if (item.getItemId() == id) { 595 return i; 596 } 597 } 598 599 return -1; 600 } 601 602 public int findGroupIndex(int group) { 603 return findGroupIndex(group, 0); 604 } 605 606 public int findGroupIndex(int group, int start) { 607 final int size = size(); 608 609 if (start < 0) { 610 start = 0; 611 } 612 613 for (int i = start; i < size; i++) { 614 final MenuItemImpl item = mItems.get(i); 615 616 if (item.getGroupId() == group) { 617 return i; 618 } 619 } 620 621 return -1; 622 } 623 624 public int size() { 625 return mItems.size(); 626 } 627 628 /** {@inheritDoc} */ 629 public MenuItem getItem(int index) { 630 return mItems.get(index); 631 } 632 633 public boolean isShortcutKey(int keyCode, KeyEvent event) { 634 return findItemWithShortcutForKey(keyCode, event) != null; 635 } 636 637 public void setQwertyMode(boolean isQwerty) { 638 mQwertyMode = isQwerty; 639 640 refreshShortcuts(isShortcutsVisible(), isQwerty); 641 } 642 643 /** 644 * Returns the ordering across all items. This will grab the category from 645 * the upper bits, find out how to order the category with respect to other 646 * categories, and combine it with the lower bits. 647 * 648 * @param categoryOrder The category order for a particular item (if it has 649 * not been or/add with a category, the default category is 650 * assumed). 651 * @return An ordering integer that can be used to order this item across 652 * all the items (even from other categories). 653 */ 654 private static int getOrdering(int categoryOrder) 655 { 656 final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT; 657 658 if (index < 0 || index >= sCategoryToOrder.length) { 659 throw new IllegalArgumentException("order does not contain a valid category."); 660 } 661 662 return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK); 663 } 664 665 /** 666 * @return whether the menu shortcuts are in qwerty mode or not 667 */ 668 boolean isQwertyMode() { 669 return mQwertyMode; 670 } 671 672 /** 673 * Refreshes the shortcut labels on each of the displayed items. Passes the arguments 674 * so submenus don't need to call their parent menu for the same values. 675 */ 676 private void refreshShortcuts(boolean shortcutsVisible, boolean qwertyMode) { 677 MenuItemImpl item; 678 for (int i = mItems.size() - 1; i >= 0; i--) { 679 item = mItems.get(i); 680 681 if (item.hasSubMenu()) { 682 ((MenuBuilder) item.getSubMenu()).refreshShortcuts(shortcutsVisible, qwertyMode); 683 } 684 685 item.refreshShortcutOnItemViews(shortcutsVisible, qwertyMode); 686 } 687 } 688 689 /** 690 * Sets whether the shortcuts should be visible on menus. 691 * 692 * @param shortcutsVisible Whether shortcuts should be visible (if true and a 693 * menu item does not have a shortcut defined, that item will 694 * still NOT show a shortcut) 695 */ 696 public void setShortcutsVisible(boolean shortcutsVisible) { 697 if (mShortcutsVisible == shortcutsVisible) return; 698 699 mShortcutsVisible = shortcutsVisible; 700 701 refreshShortcuts(shortcutsVisible, isQwertyMode()); 702 } 703 704 /** 705 * @return Whether shortcuts should be visible on menus. 706 */ 707 public boolean isShortcutsVisible() { 708 return mShortcutsVisible; 709 } 710 711 Resources getResources() { 712 return mResources; 713 } 714 715 public Callback getCallback() { 716 return mCallback; 717 } 718 719 public Context getContext() { 720 return mContext; 721 } 722 723 private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) { 724 for (int i = items.size() - 1; i >= 0; i--) { 725 MenuItemImpl item = items.get(i); 726 if (item.getOrdering() <= ordering) { 727 return i + 1; 728 } 729 } 730 731 return 0; 732 } 733 734 public boolean performShortcut(int keyCode, KeyEvent event, int flags) { 735 final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event); 736 737 boolean handled = false; 738 739 if (item != null) { 740 handled = performItemAction(item, flags); 741 } 742 743 if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) { 744 close(true); 745 } 746 747 return handled; 748 } 749 750 MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) { 751 final boolean qwerty = isQwertyMode(); 752 final int metaState = event.getMetaState(); 753 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); 754 // Get the chars associated with the keyCode (i.e using any chording combo) 755 final boolean isKeyCodeMapped = event.getKeyData(possibleChars); 756 // The delete key is not mapped to '\b' so we treat it specially 757 if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) { 758 return null; 759 } 760 761 // Look for an item whose shortcut is this key. 762 final int N = mItems.size(); 763 for (int i = 0; i < N; i++) { 764 MenuItemImpl item = mItems.get(i); 765 if (item.hasSubMenu()) { 766 MenuItemImpl subMenuItem = ((MenuBuilder)item.getSubMenu()) 767 .findItemWithShortcutForKey(keyCode, event); 768 if (subMenuItem != null) { 769 return subMenuItem; 770 } 771 } 772 if (qwerty) { 773 final char shortcutAlphaChar = item.getAlphabeticShortcut(); 774 if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) && 775 (shortcutAlphaChar != 0) && 776 (shortcutAlphaChar == possibleChars.meta[0] 777 || shortcutAlphaChar == possibleChars.meta[2] 778 || (shortcutAlphaChar == '\b' && keyCode == KeyEvent.KEYCODE_DEL)) && 779 item.isEnabled()) { 780 return item; 781 } 782 } else { 783 final char shortcutNumericChar = item.getNumericShortcut(); 784 if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) && 785 (shortcutNumericChar != 0) && 786 (shortcutNumericChar == possibleChars.meta[0] 787 || shortcutNumericChar == possibleChars.meta[2]) && 788 item.isEnabled()) { 789 return item; 790 } 791 } 792 } 793 return null; 794 } 795 796 public boolean performIdentifierAction(int id, int flags) { 797 // Look for an item whose identifier is the id. 798 return performItemAction(findItem(id), flags); 799 } 800 801 public boolean performItemAction(MenuItem item, int flags) { 802 MenuItemImpl itemImpl = (MenuItemImpl) item; 803 804 if (itemImpl == null || !itemImpl.isEnabled()) { 805 return false; 806 } 807 808 boolean invoked = itemImpl.invoke(); 809 810 if (item.hasSubMenu()) { 811 close(false); 812 813 if (mCallback != null) { 814 // Return true if the sub menu was invoked or the item was invoked previously 815 invoked = mCallback.onSubMenuSelected((SubMenuBuilder) item.getSubMenu()) 816 || invoked; 817 } 818 } else { 819 if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) { 820 close(true); 821 } 822 } 823 824 return invoked; 825 } 826 827 /** 828 * Closes the visible menu. 829 * 830 * @param allMenusAreClosing Whether the menus are completely closing (true), 831 * or whether there is another menu coming in this menu's place 832 * (false). For example, if the menu is closing because a 833 * sub menu is about to be shown, <var>allMenusAreClosing</var> 834 * is false. 835 */ 836 final void close(boolean allMenusAreClosing) { 837 Callback callback = getCallback(); 838 if (callback != null) { 839 callback.onCloseMenu(this, allMenusAreClosing); 840 } 841 } 842 843 /** {@inheritDoc} */ 844 public void close() { 845 close(true); 846 } 847 848 /** 849 * Called when an item is added or removed. 850 * 851 * @param cleared Whether the items were cleared or just changed. 852 */ 853 private void onItemsChanged(boolean cleared) { 854 if (!mPreventDispatchingItemsChanged) { 855 if (mIsVisibleItemsStale == false) mIsVisibleItemsStale = true; 856 857 MenuType[] menuTypes = mMenuTypes; 858 for (int i = 0; i < NUM_TYPES; i++) { 859 if ((menuTypes[i] != null) && (menuTypes[i].hasMenuView())) { 860 MenuView menuView = menuTypes[i].mMenuView.get(); 861 menuView.updateChildren(cleared); 862 } 863 } 864 } 865 } 866 867 /** 868 * Called by {@link MenuItemImpl} when its visible flag is changed. 869 * @param item The item that has gone through a visibility change. 870 */ 871 void onItemVisibleChanged(MenuItemImpl item) { 872 // Notify of items being changed 873 onItemsChanged(false); 874 } 875 876 ArrayList<MenuItemImpl> getVisibleItems() { 877 if (!mIsVisibleItemsStale) return mVisibleItems; 878 879 // Refresh the visible items 880 mVisibleItems.clear(); 881 882 final int itemsSize = mItems.size(); 883 MenuItemImpl item; 884 for (int i = 0; i < itemsSize; i++) { 885 item = mItems.get(i); 886 if (item.isVisible()) mVisibleItems.add(item); 887 } 888 889 mIsVisibleItemsStale = false; 890 891 return mVisibleItems; 892 } 893 894 public void clearHeader() { 895 mHeaderIcon = null; 896 mHeaderTitle = null; 897 mHeaderView = null; 898 899 onItemsChanged(false); 900 } 901 902 private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes, 903 final Drawable icon, final View view) { 904 final Resources r = getResources(); 905 906 if (view != null) { 907 mHeaderView = view; 908 909 // If using a custom view, then the title and icon aren't used 910 mHeaderTitle = null; 911 mHeaderIcon = null; 912 } else { 913 if (titleRes > 0) { 914 mHeaderTitle = r.getText(titleRes); 915 } else if (title != null) { 916 mHeaderTitle = title; 917 } 918 919 if (iconRes > 0) { 920 mHeaderIcon = r.getDrawable(iconRes); 921 } else if (icon != null) { 922 mHeaderIcon = icon; 923 } 924 925 // If using the title or icon, then a custom view isn't used 926 mHeaderView = null; 927 } 928 929 // Notify of change 930 onItemsChanged(false); 931 } 932 933 /** 934 * Sets the header's title. This replaces the header view. Called by the 935 * builder-style methods of subclasses. 936 * 937 * @param title The new title. 938 * @return This MenuBuilder so additional setters can be called. 939 */ 940 protected MenuBuilder setHeaderTitleInt(CharSequence title) { 941 setHeaderInternal(0, title, 0, null, null); 942 return this; 943 } 944 945 /** 946 * Sets the header's title. This replaces the header view. Called by the 947 * builder-style methods of subclasses. 948 * 949 * @param titleRes The new title (as a resource ID). 950 * @return This MenuBuilder so additional setters can be called. 951 */ 952 protected MenuBuilder setHeaderTitleInt(int titleRes) { 953 setHeaderInternal(titleRes, null, 0, null, null); 954 return this; 955 } 956 957 /** 958 * Sets the header's icon. This replaces the header view. Called by the 959 * builder-style methods of subclasses. 960 * 961 * @param icon The new icon. 962 * @return This MenuBuilder so additional setters can be called. 963 */ 964 protected MenuBuilder setHeaderIconInt(Drawable icon) { 965 setHeaderInternal(0, null, 0, icon, null); 966 return this; 967 } 968 969 /** 970 * Sets the header's icon. This replaces the header view. Called by the 971 * builder-style methods of subclasses. 972 * 973 * @param iconRes The new icon (as a resource ID). 974 * @return This MenuBuilder so additional setters can be called. 975 */ 976 protected MenuBuilder setHeaderIconInt(int iconRes) { 977 setHeaderInternal(0, null, iconRes, null, null); 978 return this; 979 } 980 981 /** 982 * Sets the header's view. This replaces the title and icon. Called by the 983 * builder-style methods of subclasses. 984 * 985 * @param view The new view. 986 * @return This MenuBuilder so additional setters can be called. 987 */ 988 protected MenuBuilder setHeaderViewInt(View view) { 989 setHeaderInternal(0, null, 0, null, view); 990 return this; 991 } 992 993 public CharSequence getHeaderTitle() { 994 return mHeaderTitle; 995 } 996 997 public Drawable getHeaderIcon() { 998 return mHeaderIcon; 999 } 1000 1001 public View getHeaderView() { 1002 return mHeaderView; 1003 } 1004 1005 /** 1006 * Gets the root menu (if this is a submenu, find its root menu). 1007 * @return The root menu. 1008 */ 1009 public MenuBuilder getRootMenu() { 1010 return this; 1011 } 1012 1013 /** 1014 * Sets the current menu info that is set on all items added to this menu 1015 * (until this is called again with different menu info, in which case that 1016 * one will be added to all subsequent item additions). 1017 * 1018 * @param menuInfo The extra menu information to add. 1019 */ 1020 public void setCurrentMenuInfo(ContextMenuInfo menuInfo) { 1021 mCurrentMenuInfo = menuInfo; 1022 } 1023 1024 /** 1025 * Gets an adapter for providing items and their views. 1026 * 1027 * @param menuType The type of menu to get an adapter for. 1028 * @return A {@link MenuAdapter} for this menu with the given menu type. 1029 */ 1030 public MenuAdapter getMenuAdapter(int menuType) { 1031 return new MenuAdapter(menuType); 1032 } 1033 1034 void setOptionalIconsVisible(boolean visible) { 1035 mOptionalIconsVisible = visible; 1036 } 1037 1038 boolean getOptionalIconsVisible() { 1039 return mOptionalIconsVisible; 1040 } 1041 1042 public void saveHierarchyState(Bundle outState) { 1043 SparseArray<Parcelable> viewStates = new SparseArray<Parcelable>(); 1044 1045 MenuType[] menuTypes = mMenuTypes; 1046 for (int i = NUM_TYPES - 1; i >= 0; i--) { 1047 if (menuTypes[i] == null) { 1048 continue; 1049 } 1050 1051 if (menuTypes[i].hasMenuView()) { 1052 ((View) menuTypes[i].getMenuView(null)).saveHierarchyState(viewStates); 1053 } 1054 } 1055 1056 outState.putSparseParcelableArray(VIEWS_TAG, viewStates); 1057 } 1058 1059 public void restoreHierarchyState(Bundle inState) { 1060 // Save this for menu views opened later 1061 SparseArray<Parcelable> viewStates = mFrozenViewStates = inState 1062 .getSparseParcelableArray(VIEWS_TAG); 1063 1064 // Thaw those menu views already open 1065 MenuType[] menuTypes = mMenuTypes; 1066 for (int i = NUM_TYPES - 1; i >= 0; i--) { 1067 if (menuTypes[i] == null) { 1068 continue; 1069 } 1070 1071 if (menuTypes[i].hasMenuView()) { 1072 ((View) menuTypes[i].getMenuView(null)).restoreHierarchyState(viewStates); 1073 } 1074 } 1075 } 1076 1077 /** 1078 * An adapter that allows an {@link AdapterView} to use this {@link MenuBuilder} as a data 1079 * source. This adapter will use only the visible/shown items from the menu. 1080 */ 1081 public class MenuAdapter extends BaseAdapter { 1082 private int mMenuType; 1083 1084 public MenuAdapter(int menuType) { 1085 mMenuType = menuType; 1086 } 1087 1088 public int getOffset() { 1089 if (mMenuType == TYPE_EXPANDED) { 1090 return getNumIconMenuItemsShown(); 1091 } else { 1092 return 0; 1093 } 1094 } 1095 1096 public int getCount() { 1097 return getVisibleItems().size() - getOffset(); 1098 } 1099 1100 public MenuItemImpl getItem(int position) { 1101 return getVisibleItems().get(position + getOffset()); 1102 } 1103 1104 public long getItemId(int position) { 1105 // Since a menu item's ID is optional, we'll use the position as an 1106 // ID for the item in the AdapterView 1107 return position; 1108 } 1109 1110 public View getView(int position, View convertView, ViewGroup parent) { 1111 return ((MenuItemImpl) getItem(position)).getItemView(mMenuType, parent); 1112 } 1113 1114 } 1115} 1116