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