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