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