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