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