MenuItemImpl.java revision 696cba573e651b0e4f18a4718627c8ccecb3bda0
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.ActivityNotFoundException; 22import android.content.Intent; 23import android.graphics.drawable.Drawable; 24import android.util.Log; 25import android.view.ContextMenu.ContextMenuInfo; 26import android.view.LayoutInflater; 27import android.view.MenuItem; 28import android.view.SubMenu; 29import android.view.View; 30import android.view.ViewDebug; 31 32/** 33 * @hide 34 */ 35public final class MenuItemImpl implements MenuItem { 36 private static final String TAG = "MenuItemImpl"; 37 38 private static final int SHOW_AS_ACTION_MASK = SHOW_AS_ACTION_NEVER | 39 SHOW_AS_ACTION_IF_ROOM | 40 SHOW_AS_ACTION_ALWAYS; 41 42 private final int mId; 43 private final int mGroup; 44 private final int mCategoryOrder; 45 private final int mOrdering; 46 private CharSequence mTitle; 47 private CharSequence mTitleCondensed; 48 private Intent mIntent; 49 private char mShortcutNumericChar; 50 private char mShortcutAlphabeticChar; 51 52 /** The icon's drawable which is only created as needed */ 53 private Drawable mIconDrawable; 54 /** 55 * The icon's resource ID which is used to get the Drawable when it is 56 * needed (if the Drawable isn't already obtained--only one of the two is 57 * needed). 58 */ 59 private int mIconResId = NO_ICON; 60 61 /** The menu to which this item belongs */ 62 private MenuBuilder mMenu; 63 /** If this item should launch a sub menu, this is the sub menu to launch */ 64 private SubMenuBuilder mSubMenu; 65 66 private Runnable mItemCallback; 67 private MenuItem.OnMenuItemClickListener mClickListener; 68 69 private int mFlags = ENABLED; 70 private static final int CHECKABLE = 0x00000001; 71 private static final int CHECKED = 0x00000002; 72 private static final int EXCLUSIVE = 0x00000004; 73 private static final int HIDDEN = 0x00000008; 74 private static final int ENABLED = 0x00000010; 75 private static final int IS_ACTION = 0x00000020; 76 77 private int mShowAsAction = SHOW_AS_ACTION_NEVER; 78 79 private View mActionView; 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 mMenu = menu; 126 mId = id; 127 mGroup = group; 128 mCategoryOrder = categoryOrder; 129 mOrdering = ordering; 130 mTitle = title; 131 mShowAsAction = showAsAction; 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 if (mMenu.dispatchMenuItemSelected(mMenu.getRootMenu(), this)) { 146 return true; 147 } 148 149 if (mItemCallback != null) { 150 mItemCallback.run(); 151 return true; 152 } 153 154 if (mIntent != null) { 155 try { 156 mMenu.getContext().startActivity(mIntent); 157 return true; 158 } catch (ActivityNotFoundException e) { 159 Log.e(TAG, "Can't find activity to handle intent; ignoring", e); 160 } 161 } 162 163 return false; 164 } 165 166 public boolean isEnabled() { 167 return (mFlags & ENABLED) != 0; 168 } 169 170 public MenuItem setEnabled(boolean enabled) { 171 if (enabled) { 172 mFlags |= ENABLED; 173 } else { 174 mFlags &= ~ENABLED; 175 } 176 177 mMenu.onItemsChanged(false); 178 179 return this; 180 } 181 182 public int getGroupId() { 183 return mGroup; 184 } 185 186 @ViewDebug.CapturedViewProperty 187 public int getItemId() { 188 return mId; 189 } 190 191 public int getOrder() { 192 return mCategoryOrder; 193 } 194 195 public int getOrdering() { 196 return mOrdering; 197 } 198 199 public Intent getIntent() { 200 return mIntent; 201 } 202 203 public MenuItem setIntent(Intent intent) { 204 mIntent = intent; 205 return this; 206 } 207 208 Runnable getCallback() { 209 return mItemCallback; 210 } 211 212 public MenuItem setCallback(Runnable callback) { 213 mItemCallback = callback; 214 return this; 215 } 216 217 public char getAlphabeticShortcut() { 218 return mShortcutAlphabeticChar; 219 } 220 221 public MenuItem setAlphabeticShortcut(char alphaChar) { 222 if (mShortcutAlphabeticChar == alphaChar) return this; 223 224 mShortcutAlphabeticChar = Character.toLowerCase(alphaChar); 225 226 mMenu.onItemsChanged(false); 227 228 return this; 229 } 230 231 public char getNumericShortcut() { 232 return mShortcutNumericChar; 233 } 234 235 public MenuItem setNumericShortcut(char numericChar) { 236 if (mShortcutNumericChar == numericChar) return this; 237 238 mShortcutNumericChar = numericChar; 239 240 mMenu.onItemsChanged(false); 241 242 return this; 243 } 244 245 public MenuItem setShortcut(char numericChar, char alphaChar) { 246 mShortcutNumericChar = numericChar; 247 mShortcutAlphabeticChar = Character.toLowerCase(alphaChar); 248 249 mMenu.onItemsChanged(false); 250 251 return this; 252 } 253 254 /** 255 * @return The active shortcut (based on QWERTY-mode of the menu). 256 */ 257 char getShortcut() { 258 return (mMenu.isQwertyMode() ? mShortcutAlphabeticChar : mShortcutNumericChar); 259 } 260 261 /** 262 * @return The label to show for the shortcut. This includes the chording 263 * key (for example 'Menu+a'). Also, any non-human readable 264 * characters should be human readable (for example 'Menu+enter'). 265 */ 266 String getShortcutLabel() { 267 268 char shortcut = getShortcut(); 269 if (shortcut == 0) { 270 return ""; 271 } 272 273 StringBuilder sb = new StringBuilder(sPrependShortcutLabel); 274 switch (shortcut) { 275 276 case '\n': 277 sb.append(sEnterShortcutLabel); 278 break; 279 280 case '\b': 281 sb.append(sDeleteShortcutLabel); 282 break; 283 284 case ' ': 285 sb.append(sSpaceShortcutLabel); 286 break; 287 288 default: 289 sb.append(shortcut); 290 break; 291 } 292 293 return sb.toString(); 294 } 295 296 /** 297 * @return Whether this menu item should be showing shortcuts (depends on 298 * whether the menu should show shortcuts and whether this item has 299 * a shortcut defined) 300 */ 301 boolean shouldShowShortcut() { 302 // Show shortcuts if the menu is supposed to show shortcuts AND this item has a shortcut 303 return mMenu.isShortcutsVisible() && (getShortcut() != 0); 304 } 305 306 public SubMenu getSubMenu() { 307 return mSubMenu; 308 } 309 310 public boolean hasSubMenu() { 311 return mSubMenu != null; 312 } 313 314 void setSubMenu(SubMenuBuilder subMenu) { 315 if ((mMenu != null) && (mMenu instanceof SubMenu)) { 316 throw new UnsupportedOperationException( 317 "Attempt to add a sub-menu to a sub-menu."); 318 } 319 320 mSubMenu = subMenu; 321 322 subMenu.setHeaderTitle(getTitle()); 323 } 324 325 @ViewDebug.CapturedViewProperty 326 public CharSequence getTitle() { 327 return mTitle; 328 } 329 330 /** 331 * Gets the title for a particular {@link ItemView} 332 * 333 * @param itemView The ItemView that is receiving the title 334 * @return Either the title or condensed title based on what the ItemView 335 * prefers 336 */ 337 CharSequence getTitleForItemView(MenuView.ItemView itemView) { 338 return ((itemView != null) && itemView.prefersCondensedTitle()) 339 ? getTitleCondensed() 340 : getTitle(); 341 } 342 343 public MenuItem setTitle(CharSequence title) { 344 mTitle = title; 345 346 mMenu.onItemsChanged(false); 347 348 if (mSubMenu != null) { 349 mSubMenu.setHeaderTitle(title); 350 } 351 352 return this; 353 } 354 355 public MenuItem setTitle(int title) { 356 return setTitle(mMenu.getContext().getString(title)); 357 } 358 359 public CharSequence getTitleCondensed() { 360 return mTitleCondensed != null ? mTitleCondensed : mTitle; 361 } 362 363 public MenuItem setTitleCondensed(CharSequence title) { 364 mTitleCondensed = title; 365 366 // Could use getTitle() in the loop below, but just cache what it would do here 367 if (title == null) { 368 title = mTitle; 369 } 370 371 mMenu.onItemsChanged(false); 372 373 return this; 374 } 375 376 public Drawable getIcon() { 377 if (mIconDrawable != null) { 378 return mIconDrawable; 379 } 380 381 if (mIconResId != NO_ICON) { 382 return mMenu.getResources().getDrawable(mIconResId); 383 } 384 385 return null; 386 } 387 388 public MenuItem setIcon(Drawable icon) { 389 mIconResId = NO_ICON; 390 mIconDrawable = icon; 391 mMenu.onItemsChanged(false); 392 393 return this; 394 } 395 396 public MenuItem setIcon(int iconResId) { 397 mIconDrawable = null; 398 mIconResId = iconResId; 399 400 // If we have a view, we need to push the Drawable to them 401 mMenu.onItemsChanged(false); 402 403 return this; 404 } 405 406 public boolean isCheckable() { 407 return (mFlags & CHECKABLE) == CHECKABLE; 408 } 409 410 public MenuItem setCheckable(boolean checkable) { 411 final int oldFlags = mFlags; 412 mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0); 413 if (oldFlags != mFlags) { 414 mMenu.onItemsChanged(false); 415 } 416 417 return this; 418 } 419 420 public void setExclusiveCheckable(boolean exclusive) { 421 mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0); 422 } 423 424 public boolean isExclusiveCheckable() { 425 return (mFlags & EXCLUSIVE) != 0; 426 } 427 428 public boolean isChecked() { 429 return (mFlags & CHECKED) == CHECKED; 430 } 431 432 public MenuItem setChecked(boolean checked) { 433 if ((mFlags & EXCLUSIVE) != 0) { 434 // Call the method on the Menu since it knows about the others in this 435 // exclusive checkable group 436 mMenu.setExclusiveItemChecked(this); 437 } else { 438 setCheckedInt(checked); 439 } 440 441 return this; 442 } 443 444 void setCheckedInt(boolean checked) { 445 final int oldFlags = mFlags; 446 mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0); 447 if (oldFlags != mFlags) { 448 mMenu.onItemsChanged(false); 449 } 450 } 451 452 public boolean isVisible() { 453 return (mFlags & HIDDEN) == 0; 454 } 455 456 /** 457 * Changes the visibility of the item. This method DOES NOT notify the 458 * parent menu of a change in this item, so this should only be called from 459 * methods that will eventually trigger this change. If unsure, use {@link #setVisible(boolean)} 460 * instead. 461 * 462 * @param shown Whether to show (true) or hide (false). 463 * @return Whether the item's shown state was changed 464 */ 465 boolean setVisibleInt(boolean shown) { 466 final int oldFlags = mFlags; 467 mFlags = (mFlags & ~HIDDEN) | (shown ? 0 : HIDDEN); 468 return oldFlags != mFlags; 469 } 470 471 public MenuItem setVisible(boolean shown) { 472 // Try to set the shown state to the given state. If the shown state was changed 473 // (i.e. the previous state isn't the same as given state), notify the parent menu that 474 // the shown state has changed for this item 475 if (setVisibleInt(shown)) mMenu.onItemVisibleChanged(this); 476 477 return this; 478 } 479 480 public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener clickListener) { 481 mClickListener = clickListener; 482 return this; 483 } 484 485 @Override 486 public String toString() { 487 return mTitle.toString(); 488 } 489 490 void setMenuInfo(ContextMenuInfo menuInfo) { 491 mMenuInfo = menuInfo; 492 } 493 494 public ContextMenuInfo getMenuInfo() { 495 return mMenuInfo; 496 } 497 498 /** 499 * @return Whether the menu should show icons for menu items. 500 */ 501 public boolean shouldShowIcon() { 502 return mMenu.getOptionalIconsVisible(); 503 } 504 505 public boolean isActionButton() { 506 return (mFlags & IS_ACTION) == IS_ACTION || requiresActionButton(); 507 } 508 509 public boolean requestsActionButton() { 510 return (mShowAsAction & SHOW_AS_ACTION_IF_ROOM) == SHOW_AS_ACTION_IF_ROOM; 511 } 512 513 public boolean requiresActionButton() { 514 return (mShowAsAction & SHOW_AS_ACTION_ALWAYS) == SHOW_AS_ACTION_ALWAYS; 515 } 516 517 public void setIsActionButton(boolean isActionButton) { 518 if (isActionButton) { 519 mFlags |= IS_ACTION; 520 } else { 521 mFlags &= ~IS_ACTION; 522 } 523 } 524 525 public boolean showsTextAsAction() { 526 return (mShowAsAction & SHOW_AS_ACTION_WITH_TEXT) == SHOW_AS_ACTION_WITH_TEXT; 527 } 528 529 public void setShowAsAction(int actionEnum) { 530 switch (actionEnum & SHOW_AS_ACTION_MASK) { 531 case SHOW_AS_ACTION_ALWAYS: 532 case SHOW_AS_ACTION_IF_ROOM: 533 case SHOW_AS_ACTION_NEVER: 534 // Looks good! 535 break; 536 537 default: 538 // Mutually exclusive options selected! 539 throw new IllegalArgumentException("SHOW_AS_ACTION_ALWAYS, SHOW_AS_ACTION_IF_ROOM," 540 + " and SHOW_AS_ACTION_NEVER are mutually exclusive."); 541 } 542 mShowAsAction = actionEnum; 543 mMenu.onItemActionRequestChanged(this); 544 } 545 546 public MenuItem setActionView(View view) { 547 mActionView = view; 548 mMenu.onItemActionRequestChanged(this); 549 return this; 550 } 551 552 public MenuItem setActionView(int resId) { 553 LayoutInflater inflater = LayoutInflater.from(mMenu.getContext()); 554 // TODO - Fix for proper parent. Lazily inflate in the presenter. 555 setActionView(inflater.inflate(resId, null)); 556 return this; 557 } 558 559 public View getActionView() { 560 return mActionView; 561 } 562} 563