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