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