MenuItemImpl.java revision d713f4f0643a5f6409b431f643f1ad25dba6c22f
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    @Override
184    public boolean isEnabled() {
185        return (mFlags & ENABLED) != 0;
186    }
187
188    @Override
189    public MenuItem setEnabled(boolean enabled) {
190        if (enabled) {
191            mFlags |= ENABLED;
192        } else {
193            mFlags &= ~ENABLED;
194        }
195
196        mMenu.onItemsChanged(false);
197
198        return this;
199    }
200
201    @Override
202    public int getGroupId() {
203        return mGroup;
204    }
205
206    @Override
207    @ViewDebug.CapturedViewProperty
208    public int getItemId() {
209        return mId;
210    }
211
212    @Override
213    public int getOrder() {
214        return mCategoryOrder;
215    }
216
217    public int getOrdering() {
218        return mOrdering;
219    }
220
221    @Override
222    public Intent getIntent() {
223        return mIntent;
224    }
225
226    @Override
227    public MenuItem setIntent(Intent intent) {
228        mIntent = intent;
229        return this;
230    }
231
232    Runnable getCallback() {
233        return mItemCallback;
234    }
235
236    public MenuItem setCallback(Runnable callback) {
237        mItemCallback = callback;
238        return this;
239    }
240
241    @Override
242    public char getAlphabeticShortcut() {
243        return mShortcutAlphabeticChar;
244    }
245
246    @Override
247    public MenuItem setAlphabeticShortcut(char alphaChar) {
248        if (mShortcutAlphabeticChar == alphaChar) {
249            return this;
250        }
251
252        mShortcutAlphabeticChar = Character.toLowerCase(alphaChar);
253
254        mMenu.onItemsChanged(false);
255
256        return this;
257    }
258
259    @Override
260    public char getNumericShortcut() {
261        return mShortcutNumericChar;
262    }
263
264    @Override
265    public MenuItem setNumericShortcut(char numericChar) {
266        if (mShortcutNumericChar == numericChar) {
267            return this;
268        }
269
270        mShortcutNumericChar = numericChar;
271
272        mMenu.onItemsChanged(false);
273
274        return this;
275    }
276
277    @Override
278    public MenuItem setShortcut(char numericChar, char alphaChar) {
279        mShortcutNumericChar = numericChar;
280        mShortcutAlphabeticChar = Character.toLowerCase(alphaChar);
281
282        mMenu.onItemsChanged(false);
283
284        return this;
285    }
286
287    /**
288     * @return The active shortcut (based on QWERTY-mode of the menu).
289     */
290    char getShortcut() {
291        //return (mMenu.isQwertyMode() ? mShortcutAlphabeticChar : mShortcutNumericChar);
292        return mShortcutAlphabeticChar;
293    }
294
295    /**
296     * @return The label to show for the shortcut. This includes the chording key (for example
297     *         'Menu+a'). Also, any non-human readable characters should be human readable (for
298     *         example 'Menu+enter').
299     */
300    String getShortcutLabel() {
301
302        char shortcut = getShortcut();
303        if (shortcut == 0) {
304            return "";
305        }
306
307        StringBuilder sb = new StringBuilder(sPrependShortcutLabel);
308        switch (shortcut) {
309
310            case '\n':
311                sb.append(sEnterShortcutLabel);
312                break;
313
314            case '\b':
315                sb.append(sDeleteShortcutLabel);
316                break;
317
318            case ' ':
319                sb.append(sSpaceShortcutLabel);
320                break;
321
322            default:
323                sb.append(shortcut);
324                break;
325        }
326
327        return sb.toString();
328    }
329
330    /**
331     * @return Whether this menu item should be showing shortcuts (depends on whether the menu
332     *         should show shortcuts and whether this item has a shortcut defined)
333     */
334    boolean shouldShowShortcut() {
335        // Show shortcuts if the menu is supposed to show shortcuts AND this item has a shortcut
336        return mMenu.isShortcutsVisible() && (getShortcut() != 0);
337    }
338
339    @Override
340    public SubMenu getSubMenu() {
341        return mSubMenu;
342    }
343
344    @Override
345    public boolean hasSubMenu() {
346        return mSubMenu != null;
347    }
348
349    void setSubMenu(SubMenuBuilder subMenu) {
350        mSubMenu = subMenu;
351
352        subMenu.setHeaderTitle(getTitle());
353    }
354
355    @Override
356    @ViewDebug.CapturedViewProperty
357    public CharSequence getTitle() {
358        return mTitle;
359    }
360
361    /**
362     * Gets the title for a particular {@link MenuView.ItemView}
363     *
364     * @param itemView The ItemView that is receiving the title
365     * @return Either the title or condensed title based on what the ItemView prefers
366     */
367    CharSequence getTitleForItemView(MenuView.ItemView itemView) {
368        return ((itemView != null) && itemView.prefersCondensedTitle())
369                ? getTitleCondensed()
370                : getTitle();
371    }
372
373    @Override
374    public MenuItem setTitle(CharSequence title) {
375        mTitle = title;
376
377        mMenu.onItemsChanged(false);
378
379        if (mSubMenu != null) {
380            mSubMenu.setHeaderTitle(title);
381        }
382
383        return this;
384    }
385
386    @Override
387    public MenuItem setTitle(int title) {
388        return setTitle(mMenu.getContext().getString(title));
389    }
390
391    @Override
392    public CharSequence getTitleCondensed() {
393        return mTitleCondensed != null ? mTitleCondensed : mTitle;
394    }
395
396    @Override
397    public MenuItem setTitleCondensed(CharSequence title) {
398        mTitleCondensed = title;
399
400        // Could use getTitle() in the loop below, but just cache what it would do here
401        if (title == null) {
402            title = mTitle;
403        }
404
405        mMenu.onItemsChanged(false);
406
407        return this;
408    }
409
410    @Override
411    public Drawable getIcon() {
412        if (mIconDrawable != null) {
413            return mIconDrawable;
414        }
415
416        if (mIconResId != NO_ICON) {
417            Drawable icon =  mMenu.getResources().getDrawable(mIconResId);
418            mIconResId = NO_ICON;
419            mIconDrawable = icon;
420            return icon;
421        }
422
423        return null;
424    }
425
426    @Override
427    public MenuItem setIcon(Drawable icon) {
428        mIconResId = NO_ICON;
429        mIconDrawable = icon;
430        mMenu.onItemsChanged(false);
431
432        return this;
433    }
434
435    @Override
436    public MenuItem setIcon(int iconResId) {
437        mIconDrawable = null;
438        mIconResId = iconResId;
439
440        // If we have a view, we need to push the Drawable to them
441        mMenu.onItemsChanged(false);
442
443        return this;
444    }
445
446    @Override
447    public boolean isCheckable() {
448        return (mFlags & CHECKABLE) == CHECKABLE;
449    }
450
451    @Override
452    public MenuItem setCheckable(boolean checkable) {
453        final int oldFlags = mFlags;
454        mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0);
455        if (oldFlags != mFlags) {
456            mMenu.onItemsChanged(false);
457        }
458
459        return this;
460    }
461
462    public void setExclusiveCheckable(boolean exclusive) {
463        mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0);
464    }
465
466    public boolean isExclusiveCheckable() {
467        return (mFlags & EXCLUSIVE) != 0;
468    }
469
470    @Override
471    public boolean isChecked() {
472        return (mFlags & CHECKED) == CHECKED;
473    }
474
475    @Override
476    public MenuItem setChecked(boolean checked) {
477        if ((mFlags & EXCLUSIVE) != 0) {
478            // Call the method on the Menu since it knows about the others in this
479            // exclusive checkable group
480            mMenu.setExclusiveItemChecked(this);
481        } else {
482            setCheckedInt(checked);
483        }
484
485        return this;
486    }
487
488    void setCheckedInt(boolean checked) {
489        final int oldFlags = mFlags;
490        mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0);
491        if (oldFlags != mFlags) {
492            mMenu.onItemsChanged(false);
493        }
494    }
495
496    @Override
497    public boolean isVisible() {
498        if (mActionProvider != null && mActionProvider.overridesItemVisibility()) {
499            return (mFlags & HIDDEN) == 0 && mActionProvider.isVisible();
500        }
501        return (mFlags & HIDDEN) == 0;
502    }
503
504    /**
505     * Changes the visibility of the item. This method DOES NOT notify the parent menu of a change
506     * in this item, so this should only be called from methods that will eventually trigger this
507     * change.  If unsure, use {@link #setVisible(boolean)} instead.
508     *
509     * @param shown Whether to show (true) or hide (false).
510     * @return Whether the item's shown state was changed
511     */
512    boolean setVisibleInt(boolean shown) {
513        final int oldFlags = mFlags;
514        mFlags = (mFlags & ~HIDDEN) | (shown ? 0 : HIDDEN);
515        return oldFlags != mFlags;
516    }
517
518    @Override
519    public MenuItem setVisible(boolean shown) {
520        // Try to set the shown state to the given state. If the shown state was changed
521        // (i.e. the previous state isn't the same as given state), notify the parent menu that
522        // the shown state has changed for this item
523        if (setVisibleInt(shown)) mMenu.onItemVisibleChanged(this);
524
525        return this;
526    }
527
528    @Override
529    public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener clickListener) {
530        mClickListener = clickListener;
531        return this;
532    }
533
534    @Override
535    public String toString() {
536        return mTitle.toString();
537    }
538
539    void setMenuInfo(ContextMenuInfo menuInfo) {
540        mMenuInfo = menuInfo;
541    }
542
543    @Override
544    public ContextMenuInfo getMenuInfo() {
545        return mMenuInfo;
546    }
547
548    public void actionFormatChanged() {
549        mMenu.onItemActionRequestChanged(this);
550    }
551
552    /**
553     * @return Whether the menu should show icons for menu items.
554     */
555    public boolean shouldShowIcon() {
556        return mMenu.getOptionalIconsVisible();
557    }
558
559    public boolean isActionButton() {
560        return (mFlags & IS_ACTION) == IS_ACTION;
561    }
562
563    public boolean requestsActionButton() {
564        return (mShowAsAction & SHOW_AS_ACTION_IF_ROOM) == SHOW_AS_ACTION_IF_ROOM;
565    }
566
567    public boolean requiresActionButton() {
568        return (mShowAsAction & SHOW_AS_ACTION_ALWAYS) == SHOW_AS_ACTION_ALWAYS;
569    }
570
571    public void setIsActionButton(boolean isActionButton) {
572        if (isActionButton) {
573            mFlags |= IS_ACTION;
574        } else {
575            mFlags &= ~IS_ACTION;
576        }
577    }
578
579    public boolean showsTextAsAction() {
580        return (mShowAsAction & SHOW_AS_ACTION_WITH_TEXT) == SHOW_AS_ACTION_WITH_TEXT;
581    }
582
583    @Override
584    public void setShowAsAction(int actionEnum) {
585        switch (actionEnum & SHOW_AS_ACTION_MASK) {
586            case SHOW_AS_ACTION_ALWAYS:
587            case SHOW_AS_ACTION_IF_ROOM:
588            case SHOW_AS_ACTION_NEVER:
589                // Looks good!
590                break;
591
592            default:
593                // Mutually exclusive options selected!
594                throw new IllegalArgumentException("SHOW_AS_ACTION_ALWAYS, SHOW_AS_ACTION_IF_ROOM,"
595                        + " and SHOW_AS_ACTION_NEVER are mutually exclusive.");
596        }
597        mShowAsAction = actionEnum;
598        mMenu.onItemActionRequestChanged(this);
599    }
600
601    @Override
602    public SupportMenuItem setActionView(View view) {
603        mActionView = view;
604        mActionProvider = null;
605        if (view != null && view.getId() == View.NO_ID && mId > 0) {
606            view.setId(mId);
607        }
608        mMenu.onItemActionRequestChanged(this);
609        return this;
610    }
611
612    @Override
613    public SupportMenuItem setActionView(int resId) {
614        final Context context = mMenu.getContext();
615        final LayoutInflater inflater = LayoutInflater.from(context);
616        setActionView(inflater.inflate(resId, new LinearLayout(context), false));
617        return this;
618    }
619
620    @Override
621    public View getActionView() {
622        if (mActionView != null) {
623            return mActionView;
624        } else if (mActionProvider != null) {
625            mActionView = mActionProvider.onCreateActionView(this);
626            return mActionView;
627        } else {
628            return null;
629        }
630    }
631
632    @Override
633    public MenuItem setActionProvider(android.view.ActionProvider actionProvider) {
634        throw new UnsupportedOperationException(
635                "Implementation should use setSupportActionProvider!");
636    }
637
638    @Override
639    public android.view.ActionProvider getActionProvider() {
640        throw new UnsupportedOperationException(
641                "Implementation should use getSupportActionProvider!");
642    }
643
644    @Override
645    public ActionProvider getSupportActionProvider() {
646        return mActionProvider;
647    }
648
649    @Override
650    public SupportMenuItem setSupportActionProvider(ActionProvider actionProvider) {
651        if (mActionProvider == actionProvider) {
652            return this;
653        }
654
655        mActionView = null;
656        if (mActionProvider != null) {
657            mActionProvider.setVisibilityListener(null);
658        }
659        mActionProvider = actionProvider;
660        mMenu.onItemsChanged(true); // Measurement can be changed
661        if (actionProvider != null) {
662            actionProvider.setVisibilityListener(new ActionProvider.VisibilityListener() {
663                @Override
664                public void onActionProviderVisibilityChanged(boolean isVisible) {
665                    mMenu.onItemVisibleChanged(MenuItemImpl.this);
666                }
667            });
668        }
669        return this;
670    }
671
672    @Override
673    public SupportMenuItem setShowAsActionFlags(int actionEnum) {
674        setShowAsAction(actionEnum);
675        return this;
676    }
677
678    @Override
679    public boolean expandActionView() {
680        if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0 || mActionView == null) {
681            return false;
682        }
683
684        if (mOnActionExpandListener == null ||
685                mOnActionExpandListener.onMenuItemActionExpand(this)) {
686            return mMenu.expandItemActionView(this);
687        }
688
689        return false;
690    }
691
692    @Override
693    public boolean collapseActionView() {
694        if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0) {
695            return false;
696        }
697        if (mActionView == null) {
698            // We're already collapsed if we have no action view.
699            return true;
700        }
701
702        if (mOnActionExpandListener == null ||
703                mOnActionExpandListener.onMenuItemActionCollapse(this)) {
704            return mMenu.collapseItemActionView(this);
705        }
706
707        return false;
708    }
709
710    @Override
711    public SupportMenuItem setSupportOnActionExpandListener(
712            MenuItemCompat.OnActionExpandListener listener) {
713        mOnActionExpandListener = listener;
714        return this;
715    }
716
717    public boolean hasCollapsibleActionView() {
718        return (mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) != 0 && mActionView != null;
719    }
720
721    public void setActionViewExpanded(boolean isExpanded) {
722        mIsActionViewExpanded = isExpanded;
723        mMenu.onItemsChanged(false);
724    }
725
726    @Override
727    public boolean isActionViewExpanded() {
728        return mIsActionViewExpanded;
729    }
730
731    @Override
732    public MenuItem setOnActionExpandListener(MenuItem.OnActionExpandListener listener) {
733        throw new UnsupportedOperationException(
734                "Implementation should use setSupportOnActionExpandListener!");
735    }
736}
737