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