MenuItemImpl.java revision 51ac0e94a83cfccb5105aa14df1077729a5b4ccc
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) {
168            // The action view is created by the provider in this case.
169            View actionView = getActionView();
170            mActionProvider.onPerformDefaultAction(actionView);
171            return true;
172        }
173
174        return false;
175    }
176
177    public boolean isEnabled() {
178        return (mFlags & ENABLED) != 0;
179    }
180
181    public MenuItem setEnabled(boolean enabled) {
182        if (enabled) {
183            mFlags |= ENABLED;
184        } else {
185            mFlags &= ~ENABLED;
186        }
187
188        mMenu.onItemsChanged(false);
189
190        return this;
191    }
192
193    public int getGroupId() {
194        return mGroup;
195    }
196
197    @ViewDebug.CapturedViewProperty
198    public int getItemId() {
199        return mId;
200    }
201
202    public int getOrder() {
203        return mCategoryOrder;
204    }
205
206    public int getOrdering() {
207        return mOrdering;
208    }
209
210    public Intent getIntent() {
211        return mIntent;
212    }
213
214    public MenuItem setIntent(Intent intent) {
215        mIntent = intent;
216        return this;
217    }
218
219    Runnable getCallback() {
220        return mItemCallback;
221    }
222
223    public MenuItem setCallback(Runnable callback) {
224        mItemCallback = callback;
225        return this;
226    }
227
228    public char getAlphabeticShortcut() {
229        return mShortcutAlphabeticChar;
230    }
231
232    public MenuItem setAlphabeticShortcut(char alphaChar) {
233        if (mShortcutAlphabeticChar == alphaChar) return this;
234
235        mShortcutAlphabeticChar = Character.toLowerCase(alphaChar);
236
237        mMenu.onItemsChanged(false);
238
239        return this;
240    }
241
242    public char getNumericShortcut() {
243        return mShortcutNumericChar;
244    }
245
246    public MenuItem setNumericShortcut(char numericChar) {
247        if (mShortcutNumericChar == numericChar) return this;
248
249        mShortcutNumericChar = numericChar;
250
251        mMenu.onItemsChanged(false);
252
253        return this;
254    }
255
256    public MenuItem setShortcut(char numericChar, char alphaChar) {
257        mShortcutNumericChar = numericChar;
258        mShortcutAlphabeticChar = Character.toLowerCase(alphaChar);
259
260        mMenu.onItemsChanged(false);
261
262        return this;
263    }
264
265    /**
266     * @return The active shortcut (based on QWERTY-mode of the menu).
267     */
268    char getShortcut() {
269        return (mMenu.isQwertyMode() ? mShortcutAlphabeticChar : mShortcutNumericChar);
270    }
271
272    /**
273     * @return The label to show for the shortcut. This includes the chording
274     *         key (for example 'Menu+a'). Also, any non-human readable
275     *         characters should be human readable (for example 'Menu+enter').
276     */
277    String getShortcutLabel() {
278
279        char shortcut = getShortcut();
280        if (shortcut == 0) {
281            return "";
282        }
283
284        StringBuilder sb = new StringBuilder(sPrependShortcutLabel);
285        switch (shortcut) {
286
287            case '\n':
288                sb.append(sEnterShortcutLabel);
289                break;
290
291            case '\b':
292                sb.append(sDeleteShortcutLabel);
293                break;
294
295            case ' ':
296                sb.append(sSpaceShortcutLabel);
297                break;
298
299            default:
300                sb.append(shortcut);
301                break;
302        }
303
304        return sb.toString();
305    }
306
307    /**
308     * @return Whether this menu item should be showing shortcuts (depends on
309     *         whether the menu should show shortcuts and whether this item has
310     *         a shortcut defined)
311     */
312    boolean shouldShowShortcut() {
313        // Show shortcuts if the menu is supposed to show shortcuts AND this item has a shortcut
314        return mMenu.isShortcutsVisible() && (getShortcut() != 0);
315    }
316
317    public SubMenu getSubMenu() {
318        return mSubMenu;
319    }
320
321    public boolean hasSubMenu() {
322        return mSubMenu != null;
323    }
324
325    void setSubMenu(SubMenuBuilder subMenu) {
326        if ((mMenu != null) && (mMenu instanceof SubMenu)) {
327            throw new UnsupportedOperationException(
328            "Attempt to add a sub-menu to a sub-menu.");
329        }
330
331        mSubMenu = subMenu;
332
333        subMenu.setHeaderTitle(getTitle());
334    }
335
336    @ViewDebug.CapturedViewProperty
337    public CharSequence getTitle() {
338        return mTitle;
339    }
340
341    /**
342     * Gets the title for a particular {@link ItemView}
343     *
344     * @param itemView The ItemView that is receiving the title
345     * @return Either the title or condensed title based on what the ItemView
346     *         prefers
347     */
348    CharSequence getTitleForItemView(MenuView.ItemView itemView) {
349        return ((itemView != null) && itemView.prefersCondensedTitle())
350                ? getTitleCondensed()
351                : getTitle();
352    }
353
354    public MenuItem setTitle(CharSequence title) {
355        mTitle = title;
356
357        mMenu.onItemsChanged(false);
358
359        if (mSubMenu != null) {
360            mSubMenu.setHeaderTitle(title);
361        }
362
363        return this;
364    }
365
366    public MenuItem setTitle(int title) {
367        return setTitle(mMenu.getContext().getString(title));
368    }
369
370    public CharSequence getTitleCondensed() {
371        return mTitleCondensed != null ? mTitleCondensed : mTitle;
372    }
373
374    public MenuItem setTitleCondensed(CharSequence title) {
375        mTitleCondensed = title;
376
377        // Could use getTitle() in the loop below, but just cache what it would do here
378        if (title == null) {
379            title = mTitle;
380        }
381
382        mMenu.onItemsChanged(false);
383
384        return this;
385    }
386
387    public Drawable getIcon() {
388        if (mIconDrawable != null) {
389            return mIconDrawable;
390        }
391
392        if (mIconResId != NO_ICON) {
393            return mMenu.getResources().getDrawable(mIconResId);
394        }
395
396        return null;
397    }
398
399    public MenuItem setIcon(Drawable icon) {
400        mIconResId = NO_ICON;
401        mIconDrawable = icon;
402        mMenu.onItemsChanged(false);
403
404        return this;
405    }
406
407    public MenuItem setIcon(int iconResId) {
408        mIconDrawable = null;
409        mIconResId = iconResId;
410
411        // If we have a view, we need to push the Drawable to them
412        mMenu.onItemsChanged(false);
413
414        return this;
415    }
416
417    public boolean isCheckable() {
418        return (mFlags & CHECKABLE) == CHECKABLE;
419    }
420
421    public MenuItem setCheckable(boolean checkable) {
422        final int oldFlags = mFlags;
423        mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0);
424        if (oldFlags != mFlags) {
425            mMenu.onItemsChanged(false);
426        }
427
428        return this;
429    }
430
431    public void setExclusiveCheckable(boolean exclusive) {
432        mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0);
433    }
434
435    public boolean isExclusiveCheckable() {
436        return (mFlags & EXCLUSIVE) != 0;
437    }
438
439    public boolean isChecked() {
440        return (mFlags & CHECKED) == CHECKED;
441    }
442
443    public MenuItem setChecked(boolean checked) {
444        if ((mFlags & EXCLUSIVE) != 0) {
445            // Call the method on the Menu since it knows about the others in this
446            // exclusive checkable group
447            mMenu.setExclusiveItemChecked(this);
448        } else {
449            setCheckedInt(checked);
450        }
451
452        return this;
453    }
454
455    void setCheckedInt(boolean checked) {
456        final int oldFlags = mFlags;
457        mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0);
458        if (oldFlags != mFlags) {
459            mMenu.onItemsChanged(false);
460        }
461    }
462
463    public boolean isVisible() {
464        return (mFlags & HIDDEN) == 0;
465    }
466
467    /**
468     * Changes the visibility of the item. This method DOES NOT notify the
469     * parent menu of a change in this item, so this should only be called from
470     * methods that will eventually trigger this change.  If unsure, use {@link #setVisible(boolean)}
471     * instead.
472     *
473     * @param shown Whether to show (true) or hide (false).
474     * @return Whether the item's shown state was changed
475     */
476    boolean setVisibleInt(boolean shown) {
477        final int oldFlags = mFlags;
478        mFlags = (mFlags & ~HIDDEN) | (shown ? 0 : HIDDEN);
479        return oldFlags != mFlags;
480    }
481
482    public MenuItem setVisible(boolean shown) {
483        // Try to set the shown state to the given state. If the shown state was changed
484        // (i.e. the previous state isn't the same as given state), notify the parent menu that
485        // the shown state has changed for this item
486        if (setVisibleInt(shown)) mMenu.onItemVisibleChanged(this);
487
488        return this;
489    }
490
491   public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener clickListener) {
492        mClickListener = clickListener;
493        return this;
494    }
495
496    @Override
497    public String toString() {
498        return mTitle.toString();
499    }
500
501    void setMenuInfo(ContextMenuInfo menuInfo) {
502        mMenuInfo = menuInfo;
503    }
504
505    public ContextMenuInfo getMenuInfo() {
506        return mMenuInfo;
507    }
508
509    /**
510     * @return Whether the menu should show icons for menu items.
511     */
512    public boolean shouldShowIcon() {
513        return mMenu.getOptionalIconsVisible();
514    }
515
516    public boolean isActionButton() {
517        return (mFlags & IS_ACTION) == IS_ACTION;
518    }
519
520    public boolean requestsActionButton() {
521        return (mShowAsAction & SHOW_AS_ACTION_IF_ROOM) == SHOW_AS_ACTION_IF_ROOM;
522    }
523
524    public boolean requiresActionButton() {
525        return (mShowAsAction & SHOW_AS_ACTION_ALWAYS) == SHOW_AS_ACTION_ALWAYS;
526    }
527
528    public void setIsActionButton(boolean isActionButton) {
529        if (isActionButton) {
530            mFlags |= IS_ACTION;
531        } else {
532            mFlags &= ~IS_ACTION;
533        }
534    }
535
536    public boolean showsTextAsAction() {
537        return (mShowAsAction & SHOW_AS_ACTION_WITH_TEXT) == SHOW_AS_ACTION_WITH_TEXT &&
538                mMenu.getContext().getResources().getBoolean(
539                        com.android.internal.R.bool.allow_action_menu_item_text_with_icon);
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        mMenu.onItemActionRequestChanged(this);
563        return this;
564    }
565
566    public MenuItem setActionView(int resId) {
567        final Context context = mMenu.getContext();
568        final LayoutInflater inflater = LayoutInflater.from(context);
569        setActionView(inflater.inflate(resId, new LinearLayout(context)));
570        return this;
571    }
572
573    public View getActionView() {
574        if (mActionView != null) {
575            return mActionView;
576        } else if (mActionProvider != null) {
577            mActionView = mActionProvider.onCreateActionView();
578            return mActionView;
579        } else {
580            return null;
581        }
582    }
583
584    public ActionProvider getActionProvider() {
585        return mActionProvider;
586    }
587
588    public MenuItem setActionProvider(ActionProvider actionProvider) {
589        mActionView = null;
590        mActionProvider = actionProvider;
591        mMenu.onItemsChanged(false);
592        return this;
593    }
594
595    @Override
596    public MenuItem setShowAsActionFlags(int actionEnum) {
597        setShowAsAction(actionEnum);
598        return this;
599    }
600
601    @Override
602    public boolean expandActionView() {
603        if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0 || mActionView == null) {
604            return false;
605        }
606
607        if (mOnActionExpandListener == null ||
608                mOnActionExpandListener.onMenuItemActionExpand(this)) {
609            return mMenu.expandItemActionView(this);
610        }
611
612        return false;
613    }
614
615    @Override
616    public boolean collapseActionView() {
617        if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0) {
618            return false;
619        }
620        if (mActionView == null) {
621            // We're already collapsed if we have no action view.
622            return true;
623        }
624
625        if (mOnActionExpandListener == null ||
626                mOnActionExpandListener.onMenuItemActionCollapse(this)) {
627            return mMenu.collapseItemActionView(this);
628        }
629
630        return false;
631    }
632
633    @Override
634    public MenuItem setOnActionExpandListener(OnActionExpandListener listener) {
635        mOnActionExpandListener = listener;
636        return this;
637    }
638
639    public boolean hasCollapsibleActionView() {
640        return (mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) != 0 && mActionView != null;
641    }
642
643    public void setActionViewExpanded(boolean isExpanded) {
644        mIsActionViewExpanded = isExpanded;
645        mMenu.onItemsChanged(false);
646    }
647
648    public boolean isActionViewExpanded() {
649        return mIsActionViewExpanded;
650    }
651}
652