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