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