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