MenuItemImpl.java revision 76559a65ad9d644f10beacf8895ceb217fdd0aeb
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            return mMenu.getResources().getDrawable(mIconResId);
386        }
387
388        return null;
389    }
390
391    public MenuItem setIcon(Drawable icon) {
392        mIconResId = NO_ICON;
393        mIconDrawable = icon;
394        mMenu.onItemsChanged(false);
395
396        return this;
397    }
398
399    public MenuItem setIcon(int iconResId) {
400        mIconDrawable = null;
401        mIconResId = iconResId;
402
403        // If we have a view, we need to push the Drawable to them
404        mMenu.onItemsChanged(false);
405
406        return this;
407    }
408
409    public boolean isCheckable() {
410        return (mFlags & CHECKABLE) == CHECKABLE;
411    }
412
413    public MenuItem setCheckable(boolean checkable) {
414        final int oldFlags = mFlags;
415        mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0);
416        if (oldFlags != mFlags) {
417            mMenu.onItemsChanged(false);
418        }
419
420        return this;
421    }
422
423    public void setExclusiveCheckable(boolean exclusive) {
424        mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0);
425    }
426
427    public boolean isExclusiveCheckable() {
428        return (mFlags & EXCLUSIVE) != 0;
429    }
430
431    public boolean isChecked() {
432        return (mFlags & CHECKED) == CHECKED;
433    }
434
435    public MenuItem setChecked(boolean checked) {
436        if ((mFlags & EXCLUSIVE) != 0) {
437            // Call the method on the Menu since it knows about the others in this
438            // exclusive checkable group
439            mMenu.setExclusiveItemChecked(this);
440        } else {
441            setCheckedInt(checked);
442        }
443
444        return this;
445    }
446
447    void setCheckedInt(boolean checked) {
448        final int oldFlags = mFlags;
449        mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0);
450        if (oldFlags != mFlags) {
451            mMenu.onItemsChanged(false);
452        }
453    }
454
455    public boolean isVisible() {
456        return (mFlags & HIDDEN) == 0;
457    }
458
459    /**
460     * Changes the visibility of the item. This method DOES NOT notify the
461     * parent menu of a change in this item, so this should only be called from
462     * methods that will eventually trigger this change.  If unsure, use {@link #setVisible(boolean)}
463     * instead.
464     *
465     * @param shown Whether to show (true) or hide (false).
466     * @return Whether the item's shown state was changed
467     */
468    boolean setVisibleInt(boolean shown) {
469        final int oldFlags = mFlags;
470        mFlags = (mFlags & ~HIDDEN) | (shown ? 0 : HIDDEN);
471        return oldFlags != mFlags;
472    }
473
474    public MenuItem setVisible(boolean shown) {
475        // Try to set the shown state to the given state. If the shown state was changed
476        // (i.e. the previous state isn't the same as given state), notify the parent menu that
477        // the shown state has changed for this item
478        if (setVisibleInt(shown)) mMenu.onItemVisibleChanged(this);
479
480        return this;
481    }
482
483   public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener clickListener) {
484        mClickListener = clickListener;
485        return this;
486    }
487
488    @Override
489    public String toString() {
490        return mTitle.toString();
491    }
492
493    void setMenuInfo(ContextMenuInfo menuInfo) {
494        mMenuInfo = menuInfo;
495    }
496
497    public ContextMenuInfo getMenuInfo() {
498        return mMenuInfo;
499    }
500
501    public void actionFormatChanged() {
502        mMenu.onItemActionRequestChanged(this);
503    }
504
505    /**
506     * @return Whether the menu should show icons for menu items.
507     */
508    public boolean shouldShowIcon() {
509        return mMenu.getOptionalIconsVisible();
510    }
511
512    public boolean isActionButton() {
513        return (mFlags & IS_ACTION) == IS_ACTION;
514    }
515
516    public boolean requestsActionButton() {
517        return (mShowAsAction & SHOW_AS_ACTION_IF_ROOM) == SHOW_AS_ACTION_IF_ROOM;
518    }
519
520    public boolean requiresActionButton() {
521        return (mShowAsAction & SHOW_AS_ACTION_ALWAYS) == SHOW_AS_ACTION_ALWAYS;
522    }
523
524    public void setIsActionButton(boolean isActionButton) {
525        if (isActionButton) {
526            mFlags |= IS_ACTION;
527        } else {
528            mFlags &= ~IS_ACTION;
529        }
530    }
531
532    public boolean showsTextAsAction() {
533        return (mShowAsAction & SHOW_AS_ACTION_WITH_TEXT) == SHOW_AS_ACTION_WITH_TEXT;
534    }
535
536    public void setShowAsAction(int actionEnum) {
537        switch (actionEnum & SHOW_AS_ACTION_MASK) {
538            case SHOW_AS_ACTION_ALWAYS:
539            case SHOW_AS_ACTION_IF_ROOM:
540            case SHOW_AS_ACTION_NEVER:
541                // Looks good!
542                break;
543
544            default:
545                // Mutually exclusive options selected!
546                throw new IllegalArgumentException("SHOW_AS_ACTION_ALWAYS, SHOW_AS_ACTION_IF_ROOM,"
547                        + " and SHOW_AS_ACTION_NEVER are mutually exclusive.");
548        }
549        mShowAsAction = actionEnum;
550        mMenu.onItemActionRequestChanged(this);
551    }
552
553    public MenuItem setActionView(View view) {
554        mActionView = view;
555        mActionProvider = null;
556        mMenu.onItemActionRequestChanged(this);
557        return this;
558    }
559
560    public MenuItem setActionView(int resId) {
561        final Context context = mMenu.getContext();
562        final LayoutInflater inflater = LayoutInflater.from(context);
563        setActionView(inflater.inflate(resId, new LinearLayout(context)));
564        return this;
565    }
566
567    public View getActionView() {
568        if (mActionView != null) {
569            return mActionView;
570        } else if (mActionProvider != null) {
571            mActionView = mActionProvider.onCreateActionView();
572            return mActionView;
573        } else {
574            return null;
575        }
576    }
577
578    public ActionProvider getActionProvider() {
579        return mActionProvider;
580    }
581
582    public MenuItem setActionProvider(ActionProvider actionProvider) {
583        mActionView = null;
584        mActionProvider = actionProvider;
585        mMenu.onItemsChanged(false);
586        return this;
587    }
588
589    @Override
590    public MenuItem setShowAsActionFlags(int actionEnum) {
591        setShowAsAction(actionEnum);
592        return this;
593    }
594
595    @Override
596    public boolean expandActionView() {
597        if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0 || mActionView == null) {
598            return false;
599        }
600
601        if (mOnActionExpandListener == null ||
602                mOnActionExpandListener.onMenuItemActionExpand(this)) {
603            return mMenu.expandItemActionView(this);
604        }
605
606        return false;
607    }
608
609    @Override
610    public boolean collapseActionView() {
611        if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0) {
612            return false;
613        }
614        if (mActionView == null) {
615            // We're already collapsed if we have no action view.
616            return true;
617        }
618
619        if (mOnActionExpandListener == null ||
620                mOnActionExpandListener.onMenuItemActionCollapse(this)) {
621            return mMenu.collapseItemActionView(this);
622        }
623
624        return false;
625    }
626
627    @Override
628    public MenuItem setOnActionExpandListener(OnActionExpandListener listener) {
629        mOnActionExpandListener = listener;
630        return this;
631    }
632
633    public boolean hasCollapsibleActionView() {
634        return (mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) != 0 && mActionView != null;
635    }
636
637    public void setActionViewExpanded(boolean isExpanded) {
638        mIsActionViewExpanded = isExpanded;
639        mMenu.onItemsChanged(false);
640    }
641
642    public boolean isActionViewExpanded() {
643        return mIsActionViewExpanded;
644    }
645}
646