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