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