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