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 MenuType(int menuType)172 MenuType(int menuType) { 173 mMenuType = menuType; 174 } 175 getInflater()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 getMenuView(ViewGroup parent)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 hasMenuView()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 */ onMenuItemSelected(MenuBuilder menu, MenuItem item)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 */ onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing)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 */ onSubMenuSelected(SubMenuBuilder subMenu)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 */ onCloseSubMenu(SubMenuBuilder menu)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 */ onMenuModeChange(MenuBuilder menu)263 public void onMenuModeChange(MenuBuilder menu); 264 } 265 266 /** 267 * Called by menu items to execute their associated action 268 */ 269 public interface ItemInvoker { invokeItem(MenuItemImpl item)270 public boolean invokeItem(MenuItemImpl item); 271 } 272 MenuBuilder(Context context)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 setCallback(Callback callback)288 public void setCallback(Callback callback) { 289 mCallback = callback; 290 } 291 getMenuType(int menuType)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 */ getMenuView(int menuType, ViewGroup parent)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 getNumIconMenuItemsShown()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 */ clearMenuViews()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 */ addInternal(int group, int id, int categoryOrder, CharSequence title)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 add(CharSequence title)384 public MenuItem add(CharSequence title) { 385 return addInternal(0, 0, 0, title); 386 } 387 add(int titleRes)388 public MenuItem add(int titleRes) { 389 return addInternal(0, 0, 0, mResources.getString(titleRes)); 390 } 391 add(int group, int id, int categoryOrder, CharSequence title)392 public MenuItem add(int group, int id, int categoryOrder, CharSequence title) { 393 return addInternal(group, id, categoryOrder, title); 394 } 395 add(int group, int id, int categoryOrder, int title)396 public MenuItem add(int group, int id, int categoryOrder, int title) { 397 return addInternal(group, id, categoryOrder, mResources.getString(title)); 398 } 399 addSubMenu(CharSequence title)400 public SubMenu addSubMenu(CharSequence title) { 401 return addSubMenu(0, 0, 0, title); 402 } 403 addSubMenu(int titleRes)404 public SubMenu addSubMenu(int titleRes) { 405 return addSubMenu(0, 0, 0, mResources.getString(titleRes)); 406 } 407 addSubMenu(int group, int id, int categoryOrder, CharSequence title)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 addSubMenu(int group, int id, int categoryOrder, int title)416 public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) { 417 return addSubMenu(group, id, categoryOrder, mResources.getString(title)); 418 } 419 addIntentOptions(int group, int id, int categoryOrder, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems)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 removeItem(int id)449 public void removeItem(int id) { 450 removeItemAtInt(findItemIndex(id), true); 451 } 452 removeGroup(int group)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 */ removeItemAtInt(int index, boolean updateChildrenOnMenuViews)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 removeItemAt(int index)487 public void removeItemAt(int index) { 488 removeItemAtInt(index, true); 489 } 490 clearAll()491 public void clearAll() { 492 mPreventDispatchingItemsChanged = true; 493 clear(); 494 clearHeader(); 495 mPreventDispatchingItemsChanged = false; 496 onItemsChanged(true); 497 } 498 clear()499 public void clear() { 500 mItems.clear(); 501 502 onItemsChanged(true); 503 } 504 setExclusiveItemChecked(MenuItem item)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 setGroupCheckable(int group, boolean checkable, boolean exclusive)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 setGroupVisible(int group, boolean visible)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 setGroupEnabled(int group, boolean enabled)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 hasVisibleItems()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 findItem(int id)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 findItemIndex(int id)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 findGroupIndex(int group)605 public int findGroupIndex(int group) { 606 return findGroupIndex(group, 0); 607 } 608 findGroupIndex(int group, int start)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 size()627 public int size() { 628 return mItems.size(); 629 } 630 631 /** {@inheritDoc} */ getItem(int index)632 public MenuItem getItem(int index) { 633 return mItems.get(index); 634 } 635 isShortcutKey(int keyCode, KeyEvent event)636 public boolean isShortcutKey(int keyCode, KeyEvent event) { 637 return findItemWithShortcutForKey(keyCode, event) != null; 638 } 639 setQwertyMode(boolean isQwerty)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 */ getOrdering(int categoryOrder)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 */ isQwertyMode()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 */ refreshShortcuts(boolean shortcutsVisible, boolean qwertyMode)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 */ setShortcutsVisible(boolean shortcutsVisible)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 */ isShortcutsVisible()713 public boolean isShortcutsVisible() { 714 return mShortcutsVisible; 715 } 716 getResources()717 Resources getResources() { 718 return mResources; 719 } 720 getCallback()721 public Callback getCallback() { 722 return mCallback; 723 } 724 getContext()725 public Context getContext() { 726 return mContext; 727 } 728 findInsertIndex(ArrayList<MenuItemImpl> items, int ordering)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 performShortcut(int keyCode, KeyEvent event, int flags)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 */ findItemsWithShortcutForKey(int keyCode, KeyEvent event)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 */ findItemWithShortcutForKey(int keyCode, KeyEvent event)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 performIdentifierAction(int id, int flags)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 performItemAction(MenuItem item, int flags)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 */ close(boolean allMenusAreClosing)883 final void close(boolean allMenusAreClosing) { 884 Callback callback = getCallback(); 885 if (callback != null) { 886 callback.onCloseMenu(this, allMenusAreClosing); 887 } 888 } 889 890 /** {@inheritDoc} */ close()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 */ onItemsChanged(boolean cleared)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 */ onItemVisibleChanged(MenuItemImpl item)918 void onItemVisibleChanged(MenuItemImpl item) { 919 // Notify of items being changed 920 onItemsChanged(false); 921 } 922 getVisibleItems()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 clearHeader()941 public void clearHeader() { 942 mHeaderIcon = null; 943 mHeaderTitle = null; 944 mHeaderView = null; 945 946 onItemsChanged(false); 947 } 948 setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes, final Drawable icon, final View view)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 */ setHeaderTitleInt(CharSequence title)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 */ setHeaderTitleInt(int titleRes)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 */ setHeaderIconInt(Drawable icon)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 */ setHeaderIconInt(int iconRes)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 */ setHeaderViewInt(View view)1035 protected MenuBuilder setHeaderViewInt(View view) { 1036 setHeaderInternal(0, null, 0, null, view); 1037 return this; 1038 } 1039 getHeaderTitle()1040 public CharSequence getHeaderTitle() { 1041 return mHeaderTitle; 1042 } 1043 getHeaderIcon()1044 public Drawable getHeaderIcon() { 1045 return mHeaderIcon; 1046 } 1047 getHeaderView()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 */ getRootMenu()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 */ setCurrentMenuInfo(ContextMenuInfo menuInfo)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 */ getMenuAdapter(int menuType)1077 public MenuAdapter getMenuAdapter(int menuType) { 1078 return new MenuAdapter(menuType); 1079 } 1080 setOptionalIconsVisible(boolean visible)1081 void setOptionalIconsVisible(boolean visible) { 1082 mOptionalIconsVisible = visible; 1083 } 1084 getOptionalIconsVisible()1085 boolean getOptionalIconsVisible() { 1086 return mOptionalIconsVisible; 1087 } 1088 saveHierarchyState(Bundle outState)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 restoreHierarchyState(Bundle inState)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 MenuAdapter(int menuType)1131 public MenuAdapter(int menuType) { 1132 mMenuType = menuType; 1133 } 1134 getOffset()1135 public int getOffset() { 1136 if (mMenuType == TYPE_EXPANDED) { 1137 return getNumIconMenuItemsShown(); 1138 } else { 1139 return 0; 1140 } 1141 } 1142 getCount()1143 public int getCount() { 1144 return getVisibleItems().size() - getOffset(); 1145 } 1146 getItem(int position)1147 public MenuItemImpl getItem(int position) { 1148 return getVisibleItems().get(position + getOffset()); 1149 } 1150 getItemId(int position)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 getView(int position, View convertView, ViewGroup parent)1157 public View getView(int position, View convertView, ViewGroup parent) { 1158 return ((MenuItemImpl) getItem(position)).getItemView(mMenuType, parent); 1159 } 1160 1161 } 1162 } 1163