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