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