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