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