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