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