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