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