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