MenuItemImpl.java revision 51ac0e94a83cfccb5105aa14df1077729a5b4ccc
1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.internal.view.menu; 18 19import com.android.internal.view.menu.MenuView.ItemView; 20 21import android.content.ActivityNotFoundException; 22import android.content.Context; 23import android.content.Intent; 24import android.graphics.drawable.Drawable; 25import android.util.Log; 26import android.view.ActionProvider; 27import android.view.ContextMenu.ContextMenuInfo; 28import android.view.LayoutInflater; 29import android.view.MenuItem; 30import android.view.SubMenu; 31import android.view.View; 32import android.view.ViewDebug; 33import android.widget.LinearLayout; 34 35/** 36 * @hide 37 */ 38public 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 */ 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 */ 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) { 168 // The action view is created by the provider in this case. 169 View actionView = getActionView(); 170 mActionProvider.onPerformDefaultAction(actionView); 171 return true; 172 } 173 174 return false; 175 } 176 177 public boolean isEnabled() { 178 return (mFlags & ENABLED) != 0; 179 } 180 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 193 public int getGroupId() { 194 return mGroup; 195 } 196 197 @ViewDebug.CapturedViewProperty 198 public int getItemId() { 199 return mId; 200 } 201 202 public int getOrder() { 203 return mCategoryOrder; 204 } 205 206 public int getOrdering() { 207 return mOrdering; 208 } 209 210 public Intent getIntent() { 211 return mIntent; 212 } 213 214 public MenuItem setIntent(Intent intent) { 215 mIntent = intent; 216 return this; 217 } 218 219 Runnable getCallback() { 220 return mItemCallback; 221 } 222 223 public MenuItem setCallback(Runnable callback) { 224 mItemCallback = callback; 225 return this; 226 } 227 228 public char getAlphabeticShortcut() { 229 return mShortcutAlphabeticChar; 230 } 231 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 242 public char getNumericShortcut() { 243 return mShortcutNumericChar; 244 } 245 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 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 */ 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 */ 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 */ 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 317 public SubMenu getSubMenu() { 318 return mSubMenu; 319 } 320 321 public boolean hasSubMenu() { 322 return mSubMenu != null; 323 } 324 325 void setSubMenu(SubMenuBuilder subMenu) { 326 if ((mMenu != null) && (mMenu instanceof SubMenu)) { 327 throw new UnsupportedOperationException( 328 "Attempt to add a sub-menu to a sub-menu."); 329 } 330 331 mSubMenu = subMenu; 332 333 subMenu.setHeaderTitle(getTitle()); 334 } 335 336 @ViewDebug.CapturedViewProperty 337 public CharSequence getTitle() { 338 return mTitle; 339 } 340 341 /** 342 * Gets the title for a particular {@link ItemView} 343 * 344 * @param itemView The ItemView that is receiving the title 345 * @return Either the title or condensed title based on what the ItemView 346 * prefers 347 */ 348 CharSequence getTitleForItemView(MenuView.ItemView itemView) { 349 return ((itemView != null) && itemView.prefersCondensedTitle()) 350 ? getTitleCondensed() 351 : getTitle(); 352 } 353 354 public MenuItem setTitle(CharSequence title) { 355 mTitle = title; 356 357 mMenu.onItemsChanged(false); 358 359 if (mSubMenu != null) { 360 mSubMenu.setHeaderTitle(title); 361 } 362 363 return this; 364 } 365 366 public MenuItem setTitle(int title) { 367 return setTitle(mMenu.getContext().getString(title)); 368 } 369 370 public CharSequence getTitleCondensed() { 371 return mTitleCondensed != null ? mTitleCondensed : mTitle; 372 } 373 374 public MenuItem setTitleCondensed(CharSequence title) { 375 mTitleCondensed = title; 376 377 // Could use getTitle() in the loop below, but just cache what it would do here 378 if (title == null) { 379 title = mTitle; 380 } 381 382 mMenu.onItemsChanged(false); 383 384 return this; 385 } 386 387 public Drawable getIcon() { 388 if (mIconDrawable != null) { 389 return mIconDrawable; 390 } 391 392 if (mIconResId != NO_ICON) { 393 return mMenu.getResources().getDrawable(mIconResId); 394 } 395 396 return null; 397 } 398 399 public MenuItem setIcon(Drawable icon) { 400 mIconResId = NO_ICON; 401 mIconDrawable = icon; 402 mMenu.onItemsChanged(false); 403 404 return this; 405 } 406 407 public MenuItem setIcon(int iconResId) { 408 mIconDrawable = null; 409 mIconResId = iconResId; 410 411 // If we have a view, we need to push the Drawable to them 412 mMenu.onItemsChanged(false); 413 414 return this; 415 } 416 417 public boolean isCheckable() { 418 return (mFlags & CHECKABLE) == CHECKABLE; 419 } 420 421 public MenuItem setCheckable(boolean checkable) { 422 final int oldFlags = mFlags; 423 mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0); 424 if (oldFlags != mFlags) { 425 mMenu.onItemsChanged(false); 426 } 427 428 return this; 429 } 430 431 public void setExclusiveCheckable(boolean exclusive) { 432 mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0); 433 } 434 435 public boolean isExclusiveCheckable() { 436 return (mFlags & EXCLUSIVE) != 0; 437 } 438 439 public boolean isChecked() { 440 return (mFlags & CHECKED) == CHECKED; 441 } 442 443 public MenuItem setChecked(boolean checked) { 444 if ((mFlags & EXCLUSIVE) != 0) { 445 // Call the method on the Menu since it knows about the others in this 446 // exclusive checkable group 447 mMenu.setExclusiveItemChecked(this); 448 } else { 449 setCheckedInt(checked); 450 } 451 452 return this; 453 } 454 455 void setCheckedInt(boolean checked) { 456 final int oldFlags = mFlags; 457 mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0); 458 if (oldFlags != mFlags) { 459 mMenu.onItemsChanged(false); 460 } 461 } 462 463 public boolean isVisible() { 464 return (mFlags & HIDDEN) == 0; 465 } 466 467 /** 468 * Changes the visibility of the item. This method DOES NOT notify the 469 * parent menu of a change in this item, so this should only be called from 470 * methods that will eventually trigger this change. If unsure, use {@link #setVisible(boolean)} 471 * instead. 472 * 473 * @param shown Whether to show (true) or hide (false). 474 * @return Whether the item's shown state was changed 475 */ 476 boolean setVisibleInt(boolean shown) { 477 final int oldFlags = mFlags; 478 mFlags = (mFlags & ~HIDDEN) | (shown ? 0 : HIDDEN); 479 return oldFlags != mFlags; 480 } 481 482 public MenuItem setVisible(boolean shown) { 483 // Try to set the shown state to the given state. If the shown state was changed 484 // (i.e. the previous state isn't the same as given state), notify the parent menu that 485 // the shown state has changed for this item 486 if (setVisibleInt(shown)) mMenu.onItemVisibleChanged(this); 487 488 return this; 489 } 490 491 public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener clickListener) { 492 mClickListener = clickListener; 493 return this; 494 } 495 496 @Override 497 public String toString() { 498 return mTitle.toString(); 499 } 500 501 void setMenuInfo(ContextMenuInfo menuInfo) { 502 mMenuInfo = menuInfo; 503 } 504 505 public ContextMenuInfo getMenuInfo() { 506 return mMenuInfo; 507 } 508 509 /** 510 * @return Whether the menu should show icons for menu items. 511 */ 512 public boolean shouldShowIcon() { 513 return mMenu.getOptionalIconsVisible(); 514 } 515 516 public boolean isActionButton() { 517 return (mFlags & IS_ACTION) == IS_ACTION; 518 } 519 520 public boolean requestsActionButton() { 521 return (mShowAsAction & SHOW_AS_ACTION_IF_ROOM) == SHOW_AS_ACTION_IF_ROOM; 522 } 523 524 public boolean requiresActionButton() { 525 return (mShowAsAction & SHOW_AS_ACTION_ALWAYS) == SHOW_AS_ACTION_ALWAYS; 526 } 527 528 public void setIsActionButton(boolean isActionButton) { 529 if (isActionButton) { 530 mFlags |= IS_ACTION; 531 } else { 532 mFlags &= ~IS_ACTION; 533 } 534 } 535 536 public boolean showsTextAsAction() { 537 return (mShowAsAction & SHOW_AS_ACTION_WITH_TEXT) == SHOW_AS_ACTION_WITH_TEXT && 538 mMenu.getContext().getResources().getBoolean( 539 com.android.internal.R.bool.allow_action_menu_item_text_with_icon); 540 } 541 542 public void setShowAsAction(int actionEnum) { 543 switch (actionEnum & SHOW_AS_ACTION_MASK) { 544 case SHOW_AS_ACTION_ALWAYS: 545 case SHOW_AS_ACTION_IF_ROOM: 546 case SHOW_AS_ACTION_NEVER: 547 // Looks good! 548 break; 549 550 default: 551 // Mutually exclusive options selected! 552 throw new IllegalArgumentException("SHOW_AS_ACTION_ALWAYS, SHOW_AS_ACTION_IF_ROOM," 553 + " and SHOW_AS_ACTION_NEVER are mutually exclusive."); 554 } 555 mShowAsAction = actionEnum; 556 mMenu.onItemActionRequestChanged(this); 557 } 558 559 public MenuItem setActionView(View view) { 560 mActionView = view; 561 mActionProvider = null; 562 mMenu.onItemActionRequestChanged(this); 563 return this; 564 } 565 566 public MenuItem setActionView(int resId) { 567 final Context context = mMenu.getContext(); 568 final LayoutInflater inflater = LayoutInflater.from(context); 569 setActionView(inflater.inflate(resId, new LinearLayout(context))); 570 return this; 571 } 572 573 public View getActionView() { 574 if (mActionView != null) { 575 return mActionView; 576 } else if (mActionProvider != null) { 577 mActionView = mActionProvider.onCreateActionView(); 578 return mActionView; 579 } else { 580 return null; 581 } 582 } 583 584 public ActionProvider getActionProvider() { 585 return mActionProvider; 586 } 587 588 public MenuItem setActionProvider(ActionProvider actionProvider) { 589 mActionView = null; 590 mActionProvider = actionProvider; 591 mMenu.onItemsChanged(false); 592 return this; 593 } 594 595 @Override 596 public MenuItem setShowAsActionFlags(int actionEnum) { 597 setShowAsAction(actionEnum); 598 return this; 599 } 600 601 @Override 602 public boolean expandActionView() { 603 if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0 || mActionView == null) { 604 return false; 605 } 606 607 if (mOnActionExpandListener == null || 608 mOnActionExpandListener.onMenuItemActionExpand(this)) { 609 return mMenu.expandItemActionView(this); 610 } 611 612 return false; 613 } 614 615 @Override 616 public boolean collapseActionView() { 617 if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0) { 618 return false; 619 } 620 if (mActionView == null) { 621 // We're already collapsed if we have no action view. 622 return true; 623 } 624 625 if (mOnActionExpandListener == null || 626 mOnActionExpandListener.onMenuItemActionCollapse(this)) { 627 return mMenu.collapseItemActionView(this); 628 } 629 630 return false; 631 } 632 633 @Override 634 public MenuItem setOnActionExpandListener(OnActionExpandListener listener) { 635 mOnActionExpandListener = listener; 636 return this; 637 } 638 639 public boolean hasCollapsibleActionView() { 640 return (mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) != 0 && mActionView != null; 641 } 642 643 public void setActionViewExpanded(boolean isExpanded) { 644 mIsActionViewExpanded = isExpanded; 645 mMenu.onItemsChanged(false); 646 } 647 648 public boolean isActionViewExpanded() { 649 return mIsActionViewExpanded; 650 } 651} 652