Home | History | Annotate | Download | only in menu
      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