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