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