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