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