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