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