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