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