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