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 android.support.v7.internal.view.menu; 18 19import android.content.ActivityNotFoundException; 20import android.content.Context; 21import android.content.Intent; 22import android.graphics.drawable.Drawable; 23import android.os.Build; 24import android.support.v4.content.ContextCompat; 25import android.support.v4.view.ActionProvider; 26import android.support.v4.internal.view.SupportMenuItem; 27import android.support.v4.view.MenuItemCompat; 28import android.support.v7.internal.widget.TintManager; 29import android.util.Log; 30import android.view.ContextMenu.ContextMenuInfo; 31import android.view.LayoutInflater; 32import android.view.MenuItem; 33import android.view.SubMenu; 34import android.view.View; 35import android.view.ViewDebug; 36import android.widget.LinearLayout; 37 38/** 39 * @hide 40 */ 41public final class MenuItemImpl implements SupportMenuItem { 42 43 private static final String TAG = "MenuItemImpl"; 44 45 private static final int SHOW_AS_ACTION_MASK = SHOW_AS_ACTION_NEVER | 46 SHOW_AS_ACTION_IF_ROOM | 47 SHOW_AS_ACTION_ALWAYS; 48 49 private final int mId; 50 private final int mGroup; 51 private final int mCategoryOrder; 52 private final int mOrdering; 53 private CharSequence mTitle; 54 private CharSequence mTitleCondensed; 55 private Intent mIntent; 56 private char mShortcutNumericChar; 57 private char mShortcutAlphabeticChar; 58 59 /** The icon's drawable which is only created as needed */ 60 private Drawable mIconDrawable; 61 62 /** 63 * The icon's resource ID which is used to get the Drawable when it is 64 * needed (if the Drawable isn't already obtained--only one of the two is 65 * needed). 66 */ 67 private int mIconResId = NO_ICON; 68 69 /** The menu to which this item belongs */ 70 private MenuBuilder mMenu; 71 /** If this item should launch a sub menu, this is the sub menu to launch */ 72 private SubMenuBuilder mSubMenu; 73 74 private Runnable mItemCallback; 75 private SupportMenuItem.OnMenuItemClickListener mClickListener; 76 77 private int mFlags = ENABLED; 78 private static final int CHECKABLE = 0x00000001; 79 private static final int CHECKED = 0x00000002; 80 private static final int EXCLUSIVE = 0x00000004; 81 private static final int HIDDEN = 0x00000008; 82 private static final int ENABLED = 0x00000010; 83 private static final int IS_ACTION = 0x00000020; 84 85 private int mShowAsAction = SHOW_AS_ACTION_NEVER; 86 87 private View mActionView; 88 private ActionProvider mActionProvider; 89 private MenuItemCompat.OnActionExpandListener mOnActionExpandListener; 90 private boolean mIsActionViewExpanded = false; 91 92 /** Used for the icon resource ID if this item does not have an icon */ 93 static final int NO_ICON = 0; 94 95 /** 96 * Current use case is for context menu: Extra information linked to the 97 * View that added this item to the context menu. 98 */ 99 private ContextMenuInfo mMenuInfo; 100 101 private static String sPrependShortcutLabel; 102 private static String sEnterShortcutLabel; 103 private static String sDeleteShortcutLabel; 104 private static String sSpaceShortcutLabel; 105 106 107 /** 108 * Instantiates this menu item. 109 * 110 * @param menu 111 * @param group Item ordering grouping control. The item will be added after 112 * all other items whose order is <= this number, and before any 113 * that are larger than it. This can also be used to define 114 * groups of items for batch state changes. Normally use 0. 115 * @param id Unique item ID. Use 0 if you do not need a unique ID. 116 * @param categoryOrder The ordering for this item. 117 * @param title The text to display for the item. 118 */ 119 MenuItemImpl(MenuBuilder menu, int group, int id, int categoryOrder, int ordering, 120 CharSequence title, int showAsAction) { 121 122 /*if (sPrependShortcutLabel == null) { 123 // This is instantiated from the UI thread, so no chance of sync issues 124 sPrependShortcutLabel = menu.getContext().getResources().getString( 125 com.android.internal.R.string.prepend_shortcut_label); 126 sEnterShortcutLabel = menu.getContext().getResources().getString( 127 com.android.internal.R.string.menu_enter_shortcut_label); 128 sDeleteShortcutLabel = menu.getContext().getResources().getString( 129 com.android.internal.R.string.menu_delete_shortcut_label); 130 sSpaceShortcutLabel = menu.getContext().getResources().getString( 131 com.android.internal.R.string.menu_space_shortcut_label); 132 }*/ 133 134 mMenu = menu; 135 mId = id; 136 mGroup = group; 137 mCategoryOrder = categoryOrder; 138 mOrdering = ordering; 139 mTitle = title; 140 mShowAsAction = showAsAction; 141 } 142 143 /** 144 * Invokes the item by calling various listeners or callbacks. 145 * 146 * @return true if the invocation was handled, false otherwise 147 */ 148 public boolean invoke() { 149 if (mClickListener != null && mClickListener.onMenuItemClick(this)) { 150 return true; 151 } 152 153 if (mMenu.dispatchMenuItemSelected(mMenu.getRootMenu(), this)) { 154 return true; 155 } 156 157 if (mItemCallback != null) { 158 mItemCallback.run(); 159 return true; 160 } 161 162 if (mIntent != null) { 163 try { 164 mMenu.getContext().startActivity(mIntent); 165 return true; 166 } catch (ActivityNotFoundException e) { 167 Log.e(TAG, "Can't find activity to handle intent; ignoring", e); 168 } 169 } 170 171 if (mActionProvider != null && mActionProvider.onPerformDefaultAction()) { 172 return true; 173 } 174 175 return false; 176 } 177 178 @Override 179 public boolean isEnabled() { 180 return (mFlags & ENABLED) != 0; 181 } 182 183 @Override 184 public MenuItem setEnabled(boolean enabled) { 185 if (enabled) { 186 mFlags |= ENABLED; 187 } else { 188 mFlags &= ~ENABLED; 189 } 190 191 mMenu.onItemsChanged(false); 192 193 return this; 194 } 195 196 @Override 197 public int getGroupId() { 198 return mGroup; 199 } 200 201 @Override 202 @ViewDebug.CapturedViewProperty 203 public int getItemId() { 204 return mId; 205 } 206 207 @Override 208 public int getOrder() { 209 return mCategoryOrder; 210 } 211 212 public int getOrdering() { 213 return mOrdering; 214 } 215 216 @Override 217 public Intent getIntent() { 218 return mIntent; 219 } 220 221 @Override 222 public MenuItem setIntent(Intent intent) { 223 mIntent = intent; 224 return this; 225 } 226 227 Runnable getCallback() { 228 return mItemCallback; 229 } 230 231 public MenuItem setCallback(Runnable callback) { 232 mItemCallback = callback; 233 return this; 234 } 235 236 @Override 237 public char getAlphabeticShortcut() { 238 return mShortcutAlphabeticChar; 239 } 240 241 @Override 242 public MenuItem setAlphabeticShortcut(char alphaChar) { 243 if (mShortcutAlphabeticChar == alphaChar) { 244 return this; 245 } 246 247 mShortcutAlphabeticChar = Character.toLowerCase(alphaChar); 248 249 mMenu.onItemsChanged(false); 250 251 return this; 252 } 253 254 @Override 255 public char getNumericShortcut() { 256 return mShortcutNumericChar; 257 } 258 259 @Override 260 public MenuItem setNumericShortcut(char numericChar) { 261 if (mShortcutNumericChar == numericChar) { 262 return this; 263 } 264 265 mShortcutNumericChar = numericChar; 266 267 mMenu.onItemsChanged(false); 268 269 return this; 270 } 271 272 @Override 273 public MenuItem setShortcut(char numericChar, char alphaChar) { 274 mShortcutNumericChar = numericChar; 275 mShortcutAlphabeticChar = Character.toLowerCase(alphaChar); 276 277 mMenu.onItemsChanged(false); 278 279 return this; 280 } 281 282 /** 283 * @return The active shortcut (based on QWERTY-mode of the menu). 284 */ 285 char getShortcut() { 286 return (mMenu.isQwertyMode() ? mShortcutAlphabeticChar : mShortcutNumericChar); 287 } 288 289 /** 290 * @return The label to show for the shortcut. This includes the chording key (for example 291 * 'Menu+a'). Also, any non-human readable characters should be human readable (for 292 * example 'Menu+enter'). 293 */ 294 String getShortcutLabel() { 295 296 char shortcut = getShortcut(); 297 if (shortcut == 0) { 298 return ""; 299 } 300 301 StringBuilder sb = new StringBuilder(sPrependShortcutLabel); 302 switch (shortcut) { 303 304 case '\n': 305 sb.append(sEnterShortcutLabel); 306 break; 307 308 case '\b': 309 sb.append(sDeleteShortcutLabel); 310 break; 311 312 case ' ': 313 sb.append(sSpaceShortcutLabel); 314 break; 315 316 default: 317 sb.append(shortcut); 318 break; 319 } 320 321 return sb.toString(); 322 } 323 324 /** 325 * @return Whether this menu item should be showing shortcuts (depends on 326 * whether the menu should show shortcuts and whether this item has 327 * a shortcut defined) 328 */ 329 boolean shouldShowShortcut() { 330 // Show shortcuts if the menu is supposed to show shortcuts AND this item has a shortcut 331 return mMenu.isShortcutsVisible() && (getShortcut() != 0); 332 } 333 334 @Override 335 public SubMenu getSubMenu() { 336 return mSubMenu; 337 } 338 339 @Override 340 public boolean hasSubMenu() { 341 return mSubMenu != null; 342 } 343 344 public void setSubMenu(SubMenuBuilder subMenu) { 345 mSubMenu = subMenu; 346 347 subMenu.setHeaderTitle(getTitle()); 348 } 349 350 @Override 351 @ViewDebug.CapturedViewProperty 352 public CharSequence getTitle() { 353 return mTitle; 354 } 355 356 /** 357 * Gets the title for a particular {@link MenuView.ItemView} 358 * 359 * @param itemView The ItemView that is receiving the title 360 * @return Either the title or condensed title based on what the ItemView prefers 361 */ 362 CharSequence getTitleForItemView(MenuView.ItemView itemView) { 363 return ((itemView != null) && itemView.prefersCondensedTitle()) 364 ? getTitleCondensed() 365 : getTitle(); 366 } 367 368 @Override 369 public MenuItem setTitle(CharSequence title) { 370 mTitle = title; 371 372 mMenu.onItemsChanged(false); 373 374 if (mSubMenu != null) { 375 mSubMenu.setHeaderTitle(title); 376 } 377 378 return this; 379 } 380 381 @Override 382 public MenuItem setTitle(int title) { 383 return setTitle(mMenu.getContext().getString(title)); 384 } 385 386 @Override 387 public CharSequence getTitleCondensed() { 388 final CharSequence ctitle = mTitleCondensed != null ? mTitleCondensed : mTitle; 389 390 if (Build.VERSION.SDK_INT < 18 && ctitle != null && !(ctitle instanceof String)) { 391 // For devices pre-JB-MR2, where we have a non-String CharSequence, we need to 392 // convert this to a String so that EventLog.writeEvent() does not throw an exception 393 // in Activity.onMenuItemSelected() 394 return ctitle.toString(); 395 } else { 396 // Else, we just return the condensed title 397 return ctitle; 398 } 399 } 400 401 @Override 402 public MenuItem setTitleCondensed(CharSequence title) { 403 mTitleCondensed = title; 404 405 // Could use getTitle() in the loop below, but just cache what it would do here 406 if (title == null) { 407 title = mTitle; 408 } 409 410 mMenu.onItemsChanged(false); 411 412 return this; 413 } 414 415 @Override 416 public Drawable getIcon() { 417 if (mIconDrawable != null) { 418 return mIconDrawable; 419 } 420 421 if (mIconResId != NO_ICON) { 422 Drawable icon = TintManager.getDrawable(mMenu.getContext(), mIconResId); 423 mIconResId = NO_ICON; 424 mIconDrawable = icon; 425 return icon; 426 } 427 428 return null; 429 } 430 431 @Override 432 public MenuItem setIcon(Drawable icon) { 433 mIconResId = NO_ICON; 434 mIconDrawable = icon; 435 mMenu.onItemsChanged(false); 436 437 return this; 438 } 439 440 @Override 441 public MenuItem setIcon(int iconResId) { 442 mIconDrawable = null; 443 mIconResId = iconResId; 444 445 // If we have a view, we need to push the Drawable to them 446 mMenu.onItemsChanged(false); 447 448 return this; 449 } 450 451 @Override 452 public boolean isCheckable() { 453 return (mFlags & CHECKABLE) == CHECKABLE; 454 } 455 456 @Override 457 public MenuItem setCheckable(boolean checkable) { 458 final int oldFlags = mFlags; 459 mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0); 460 if (oldFlags != mFlags) { 461 mMenu.onItemsChanged(false); 462 } 463 464 return this; 465 } 466 467 public void setExclusiveCheckable(boolean exclusive) { 468 mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0); 469 } 470 471 public boolean isExclusiveCheckable() { 472 return (mFlags & EXCLUSIVE) != 0; 473 } 474 475 @Override 476 public boolean isChecked() { 477 return (mFlags & CHECKED) == CHECKED; 478 } 479 480 @Override 481 public MenuItem setChecked(boolean checked) { 482 if ((mFlags & EXCLUSIVE) != 0) { 483 // Call the method on the Menu since it knows about the others in this 484 // exclusive checkable group 485 mMenu.setExclusiveItemChecked(this); 486 } else { 487 setCheckedInt(checked); 488 } 489 490 return this; 491 } 492 493 void setCheckedInt(boolean checked) { 494 final int oldFlags = mFlags; 495 mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0); 496 if (oldFlags != mFlags) { 497 mMenu.onItemsChanged(false); 498 } 499 } 500 501 @Override 502 public boolean isVisible() { 503 if (mActionProvider != null && mActionProvider.overridesItemVisibility()) { 504 return (mFlags & HIDDEN) == 0 && mActionProvider.isVisible(); 505 } 506 return (mFlags & HIDDEN) == 0; 507 } 508 509 /** 510 * Changes the visibility of the item. This method DOES NOT notify the parent menu of a change 511 * in this item, so this should only be called from methods that will eventually trigger this 512 * change. If unsure, use {@link #setVisible(boolean)} instead. 513 * 514 * @param shown Whether to show (true) or hide (false). 515 * @return Whether the item's shown state was changed 516 */ 517 boolean setVisibleInt(boolean shown) { 518 final int oldFlags = mFlags; 519 mFlags = (mFlags & ~HIDDEN) | (shown ? 0 : HIDDEN); 520 return oldFlags != mFlags; 521 } 522 523 @Override 524 public MenuItem setVisible(boolean shown) { 525 // Try to set the shown state to the given state. If the shown state was changed 526 // (i.e. the previous state isn't the same as given state), notify the parent menu that 527 // the shown state has changed for this item 528 if (setVisibleInt(shown)) mMenu.onItemVisibleChanged(this); 529 530 return this; 531 } 532 533 @Override 534 public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener clickListener) { 535 mClickListener = clickListener; 536 return this; 537 } 538 539 @Override 540 public String toString() { 541 return mTitle.toString(); 542 } 543 544 void setMenuInfo(ContextMenuInfo menuInfo) { 545 mMenuInfo = menuInfo; 546 } 547 548 @Override 549 public ContextMenuInfo getMenuInfo() { 550 return mMenuInfo; 551 } 552 553 public void actionFormatChanged() { 554 mMenu.onItemActionRequestChanged(this); 555 } 556 557 /** 558 * @return Whether the menu should show icons for menu items. 559 */ 560 public boolean shouldShowIcon() { 561 return mMenu.getOptionalIconsVisible(); 562 } 563 564 public boolean isActionButton() { 565 return (mFlags & IS_ACTION) == IS_ACTION; 566 } 567 568 public boolean requestsActionButton() { 569 return (mShowAsAction & SHOW_AS_ACTION_IF_ROOM) == SHOW_AS_ACTION_IF_ROOM; 570 } 571 572 public boolean requiresActionButton() { 573 return (mShowAsAction & SHOW_AS_ACTION_ALWAYS) == SHOW_AS_ACTION_ALWAYS; 574 } 575 576 public void setIsActionButton(boolean isActionButton) { 577 if (isActionButton) { 578 mFlags |= IS_ACTION; 579 } else { 580 mFlags &= ~IS_ACTION; 581 } 582 } 583 584 public boolean showsTextAsAction() { 585 return (mShowAsAction & SHOW_AS_ACTION_WITH_TEXT) == SHOW_AS_ACTION_WITH_TEXT; 586 } 587 588 @Override 589 public void setShowAsAction(int actionEnum) { 590 switch (actionEnum & SHOW_AS_ACTION_MASK) { 591 case SHOW_AS_ACTION_ALWAYS: 592 case SHOW_AS_ACTION_IF_ROOM: 593 case SHOW_AS_ACTION_NEVER: 594 // Looks good! 595 break; 596 597 default: 598 // Mutually exclusive options selected! 599 throw new IllegalArgumentException("SHOW_AS_ACTION_ALWAYS, SHOW_AS_ACTION_IF_ROOM," 600 + " and SHOW_AS_ACTION_NEVER are mutually exclusive."); 601 } 602 mShowAsAction = actionEnum; 603 mMenu.onItemActionRequestChanged(this); 604 } 605 606 @Override 607 public SupportMenuItem setActionView(View view) { 608 mActionView = view; 609 mActionProvider = null; 610 if (view != null && view.getId() == View.NO_ID && mId > 0) { 611 view.setId(mId); 612 } 613 mMenu.onItemActionRequestChanged(this); 614 return this; 615 } 616 617 @Override 618 public SupportMenuItem setActionView(int resId) { 619 final Context context = mMenu.getContext(); 620 final LayoutInflater inflater = LayoutInflater.from(context); 621 setActionView(inflater.inflate(resId, new LinearLayout(context), false)); 622 return this; 623 } 624 625 @Override 626 public View getActionView() { 627 if (mActionView != null) { 628 return mActionView; 629 } else if (mActionProvider != null) { 630 mActionView = mActionProvider.onCreateActionView(this); 631 return mActionView; 632 } else { 633 return null; 634 } 635 } 636 637 @Override 638 public MenuItem setActionProvider(android.view.ActionProvider actionProvider) { 639 throw new UnsupportedOperationException( 640 "This is not supported, use MenuItemCompat.setActionProvider()"); 641 } 642 643 @Override 644 public android.view.ActionProvider getActionProvider() { 645 throw new UnsupportedOperationException( 646 "This is not supported, use MenuItemCompat.getActionProvider()"); 647 } 648 649 @Override 650 public ActionProvider getSupportActionProvider() { 651 return mActionProvider; 652 } 653 654 @Override 655 public SupportMenuItem setSupportActionProvider(ActionProvider actionProvider) { 656 if (mActionProvider != null) { 657 mActionProvider.reset(); 658 } 659 mActionView = null; 660 mActionProvider = actionProvider; 661 mMenu.onItemsChanged(true); // Measurement can be changed 662 if (mActionProvider != null) { 663 mActionProvider.setVisibilityListener(new ActionProvider.VisibilityListener() { 664 @Override 665 public void onActionProviderVisibilityChanged(boolean isVisible) { 666 mMenu.onItemVisibleChanged(MenuItemImpl.this); 667 } 668 }); 669 } 670 return this; 671 } 672 673 @Override 674 public SupportMenuItem setShowAsActionFlags(int actionEnum) { 675 setShowAsAction(actionEnum); 676 return this; 677 } 678 679 @Override 680 public boolean expandActionView() { 681 if (!hasCollapsibleActionView()) { 682 return false; 683 } 684 685 if (mOnActionExpandListener == null || 686 mOnActionExpandListener.onMenuItemActionExpand(this)) { 687 return mMenu.expandItemActionView(this); 688 } 689 690 return false; 691 } 692 693 @Override 694 public boolean collapseActionView() { 695 if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0) { 696 return false; 697 } 698 if (mActionView == null) { 699 // We're already collapsed if we have no action view. 700 return true; 701 } 702 703 if (mOnActionExpandListener == null || 704 mOnActionExpandListener.onMenuItemActionCollapse(this)) { 705 return mMenu.collapseItemActionView(this); 706 } 707 708 return false; 709 } 710 711 @Override 712 public SupportMenuItem setSupportOnActionExpandListener( 713 MenuItemCompat.OnActionExpandListener listener) { 714 mOnActionExpandListener = listener; 715 return this; 716 } 717 718 public boolean hasCollapsibleActionView() { 719 if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) != 0) { 720 if (mActionView == null && mActionProvider != null) { 721 mActionView = mActionProvider.onCreateActionView(this); 722 } 723 return mActionView != null; 724 } 725 return false; 726 } 727 728 public void setActionViewExpanded(boolean isExpanded) { 729 mIsActionViewExpanded = isExpanded; 730 mMenu.onItemsChanged(false); 731 } 732 733 @Override 734 public boolean isActionViewExpanded() { 735 return mIsActionViewExpanded; 736 } 737 738 @Override 739 public MenuItem setOnActionExpandListener(MenuItem.OnActionExpandListener listener) { 740 throw new UnsupportedOperationException( 741 "This is not supported, use MenuItemCompat.setOnActionExpandListener()"); 742 } 743} 744