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