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