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