MenuItemImpl.java revision 8eea3ea5591e59f55cbb4f6b2b7e9363a285ced3
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 sLanguage;
97    private static String sPrependShortcutLabel;
98    private static String sEnterShortcutLabel;
99    private static String sDeleteShortcutLabel;
100    private static String sSpaceShortcutLabel;
101
102
103    /**
104     * Instantiates this menu item.
105     *
106     * @param menu
107     * @param group Item ordering grouping control. The item will be added after
108     *            all other items whose order is <= this number, and before any
109     *            that are larger than it. This can also be used to define
110     *            groups of items for batch state changes. Normally use 0.
111     * @param id Unique item ID. Use 0 if you do not need a unique ID.
112     * @param categoryOrder The ordering for this item.
113     * @param title The text to display for the item.
114     */
115    MenuItemImpl(MenuBuilder menu, int group, int id, int categoryOrder, int ordering,
116            CharSequence title, int showAsAction) {
117
118        String lang = menu.getContext().getResources().getConfiguration().locale.toString();
119        if (sPrependShortcutLabel == null || !lang.equals(sLanguage)) {
120            sLanguage = lang;
121            // This is instantiated from the UI thread, so no chance of sync issues
122            sPrependShortcutLabel = menu.getContext().getResources().getString(
123                    com.android.internal.R.string.prepend_shortcut_label);
124            sEnterShortcutLabel = menu.getContext().getResources().getString(
125                    com.android.internal.R.string.menu_enter_shortcut_label);
126            sDeleteShortcutLabel = menu.getContext().getResources().getString(
127                    com.android.internal.R.string.menu_delete_shortcut_label);
128            sSpaceShortcutLabel = menu.getContext().getResources().getString(
129                    com.android.internal.R.string.menu_space_shortcut_label);
130        }
131
132        mMenu = menu;
133        mId = id;
134        mGroup = group;
135        mCategoryOrder = categoryOrder;
136        mOrdering = ordering;
137        mTitle = title;
138        mShowAsAction = showAsAction;
139    }
140
141    /**
142     * Invokes the item by calling various listeners or callbacks.
143     *
144     * @return true if the invocation was handled, false otherwise
145     */
146    public boolean invoke() {
147        if (mClickListener != null &&
148            mClickListener.onMenuItemClick(this)) {
149            return true;
150        }
151
152        if (mMenu.dispatchMenuItemSelected(mMenu.getRootMenu(), this)) {
153            return true;
154        }
155
156        if (mItemCallback != null) {
157            mItemCallback.run();
158            return true;
159        }
160
161        if (mIntent != null) {
162            try {
163                mMenu.getContext().startActivity(mIntent);
164                return true;
165            } catch (ActivityNotFoundException e) {
166                Log.e(TAG, "Can't find activity to handle intent; ignoring", e);
167            }
168        }
169
170        if (mActionProvider != null && mActionProvider.onPerformDefaultAction()) {
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        mSubMenu = subMenu;
327
328        subMenu.setHeaderTitle(getTitle());
329    }
330
331    @ViewDebug.CapturedViewProperty
332    public CharSequence getTitle() {
333        return mTitle;
334    }
335
336    /**
337     * Gets the title for a particular {@link ItemView}
338     *
339     * @param itemView The ItemView that is receiving the title
340     * @return Either the title or condensed title based on what the ItemView
341     *         prefers
342     */
343    CharSequence getTitleForItemView(MenuView.ItemView itemView) {
344        return ((itemView != null) && itemView.prefersCondensedTitle())
345                ? getTitleCondensed()
346                : getTitle();
347    }
348
349    public MenuItem setTitle(CharSequence title) {
350        mTitle = title;
351
352        mMenu.onItemsChanged(false);
353
354        if (mSubMenu != null) {
355            mSubMenu.setHeaderTitle(title);
356        }
357
358        return this;
359    }
360
361    public MenuItem setTitle(int title) {
362        return setTitle(mMenu.getContext().getString(title));
363    }
364
365    public CharSequence getTitleCondensed() {
366        return mTitleCondensed != null ? mTitleCondensed : mTitle;
367    }
368
369    public MenuItem setTitleCondensed(CharSequence title) {
370        mTitleCondensed = title;
371
372        // Could use getTitle() in the loop below, but just cache what it would do here
373        if (title == null) {
374            title = mTitle;
375        }
376
377        mMenu.onItemsChanged(false);
378
379        return this;
380    }
381
382    public Drawable getIcon() {
383        if (mIconDrawable != null) {
384            return mIconDrawable;
385        }
386
387        if (mIconResId != NO_ICON) {
388            Drawable icon =  mMenu.getContext().getDrawable(mIconResId);
389            mIconResId = NO_ICON;
390            mIconDrawable = icon;
391            return icon;
392        }
393
394        return null;
395    }
396
397    public MenuItem setIcon(Drawable icon) {
398        mIconResId = NO_ICON;
399        mIconDrawable = icon;
400        mMenu.onItemsChanged(false);
401
402        return this;
403    }
404
405    public MenuItem setIcon(int iconResId) {
406        mIconDrawable = null;
407        mIconResId = iconResId;
408
409        // If we have a view, we need to push the Drawable to them
410        mMenu.onItemsChanged(false);
411
412        return this;
413    }
414
415    public boolean isCheckable() {
416        return (mFlags & CHECKABLE) == CHECKABLE;
417    }
418
419    public MenuItem setCheckable(boolean checkable) {
420        final int oldFlags = mFlags;
421        mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0);
422        if (oldFlags != mFlags) {
423            mMenu.onItemsChanged(false);
424        }
425
426        return this;
427    }
428
429    public void setExclusiveCheckable(boolean exclusive) {
430        mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0);
431    }
432
433    public boolean isExclusiveCheckable() {
434        return (mFlags & EXCLUSIVE) != 0;
435    }
436
437    public boolean isChecked() {
438        return (mFlags & CHECKED) == CHECKED;
439    }
440
441    public MenuItem setChecked(boolean checked) {
442        if ((mFlags & EXCLUSIVE) != 0) {
443            // Call the method on the Menu since it knows about the others in this
444            // exclusive checkable group
445            mMenu.setExclusiveItemChecked(this);
446        } else {
447            setCheckedInt(checked);
448        }
449
450        return this;
451    }
452
453    void setCheckedInt(boolean checked) {
454        final int oldFlags = mFlags;
455        mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0);
456        if (oldFlags != mFlags) {
457            mMenu.onItemsChanged(false);
458        }
459    }
460
461    public boolean isVisible() {
462        if (mActionProvider != null && mActionProvider.overridesItemVisibility()) {
463            return (mFlags & HIDDEN) == 0 && mActionProvider.isVisible();
464        }
465        return (mFlags & HIDDEN) == 0;
466    }
467
468    /**
469     * Changes the visibility of the item. This method DOES NOT notify the
470     * parent menu of a change in this item, so this should only be called from
471     * methods that will eventually trigger this change.  If unsure, use {@link #setVisible(boolean)}
472     * instead.
473     *
474     * @param shown Whether to show (true) or hide (false).
475     * @return Whether the item's shown state was changed
476     */
477    boolean setVisibleInt(boolean shown) {
478        final int oldFlags = mFlags;
479        mFlags = (mFlags & ~HIDDEN) | (shown ? 0 : HIDDEN);
480        return oldFlags != mFlags;
481    }
482
483    public MenuItem setVisible(boolean shown) {
484        // Try to set the shown state to the given state. If the shown state was changed
485        // (i.e. the previous state isn't the same as given state), notify the parent menu that
486        // the shown state has changed for this item
487        if (setVisibleInt(shown)) mMenu.onItemVisibleChanged(this);
488
489        return this;
490    }
491
492   public MenuItem setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener clickListener) {
493        mClickListener = clickListener;
494        return this;
495    }
496
497    @Override
498    public String toString() {
499        return mTitle.toString();
500    }
501
502    void setMenuInfo(ContextMenuInfo menuInfo) {
503        mMenuInfo = menuInfo;
504    }
505
506    public ContextMenuInfo getMenuInfo() {
507        return mMenuInfo;
508    }
509
510    public void actionFormatChanged() {
511        mMenu.onItemActionRequestChanged(this);
512    }
513
514    /**
515     * @return Whether the menu should show icons for menu items.
516     */
517    public boolean shouldShowIcon() {
518        return mMenu.getOptionalIconsVisible();
519    }
520
521    public boolean isActionButton() {
522        return (mFlags & IS_ACTION) == IS_ACTION;
523    }
524
525    public boolean requestsActionButton() {
526        return (mShowAsAction & SHOW_AS_ACTION_IF_ROOM) == SHOW_AS_ACTION_IF_ROOM;
527    }
528
529    public boolean requiresActionButton() {
530        return (mShowAsAction & SHOW_AS_ACTION_ALWAYS) == SHOW_AS_ACTION_ALWAYS;
531    }
532
533    public void setIsActionButton(boolean isActionButton) {
534        if (isActionButton) {
535            mFlags |= IS_ACTION;
536        } else {
537            mFlags &= ~IS_ACTION;
538        }
539    }
540
541    public boolean showsTextAsAction() {
542        return (mShowAsAction & SHOW_AS_ACTION_WITH_TEXT) == SHOW_AS_ACTION_WITH_TEXT;
543    }
544
545    public void setShowAsAction(int actionEnum) {
546        switch (actionEnum & SHOW_AS_ACTION_MASK) {
547            case SHOW_AS_ACTION_ALWAYS:
548            case SHOW_AS_ACTION_IF_ROOM:
549            case SHOW_AS_ACTION_NEVER:
550                // Looks good!
551                break;
552
553            default:
554                // Mutually exclusive options selected!
555                throw new IllegalArgumentException("SHOW_AS_ACTION_ALWAYS, SHOW_AS_ACTION_IF_ROOM,"
556                        + " and SHOW_AS_ACTION_NEVER are mutually exclusive.");
557        }
558        mShowAsAction = actionEnum;
559        mMenu.onItemActionRequestChanged(this);
560    }
561
562    public MenuItem setActionView(View view) {
563        mActionView = view;
564        mActionProvider = null;
565        if (view != null && view.getId() == View.NO_ID && mId > 0) {
566            view.setId(mId);
567        }
568        mMenu.onItemActionRequestChanged(this);
569        return this;
570    }
571
572    public MenuItem setActionView(int resId) {
573        final Context context = mMenu.getContext();
574        final LayoutInflater inflater = LayoutInflater.from(context);
575        setActionView(inflater.inflate(resId, new LinearLayout(context), false));
576        return this;
577    }
578
579    public View getActionView() {
580        if (mActionView != null) {
581            return mActionView;
582        } else if (mActionProvider != null) {
583            mActionView = mActionProvider.onCreateActionView(this);
584            return mActionView;
585        } else {
586            return null;
587        }
588    }
589
590    public ActionProvider getActionProvider() {
591        return mActionProvider;
592    }
593
594    public MenuItem setActionProvider(ActionProvider actionProvider) {
595        if (mActionProvider != null) {
596            mActionProvider.setVisibilityListener(null);
597        }
598        mActionView = null;
599        mActionProvider = actionProvider;
600        mMenu.onItemsChanged(true); // Measurement can be changed
601        if (mActionProvider != null) {
602            mActionProvider.setVisibilityListener(new ActionProvider.VisibilityListener() {
603                @Override public void onActionProviderVisibilityChanged(boolean isVisible) {
604                    mMenu.onItemVisibleChanged(MenuItemImpl.this);
605                }
606            });
607        }
608        return this;
609    }
610
611    @Override
612    public MenuItem setShowAsActionFlags(int actionEnum) {
613        setShowAsAction(actionEnum);
614        return this;
615    }
616
617    @Override
618    public boolean expandActionView() {
619        if (!hasCollapsibleActionView()) {
620            return false;
621        }
622
623        if (mOnActionExpandListener == null ||
624                mOnActionExpandListener.onMenuItemActionExpand(this)) {
625            return mMenu.expandItemActionView(this);
626        }
627
628        return false;
629    }
630
631    @Override
632    public boolean collapseActionView() {
633        if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0) {
634            return false;
635        }
636        if (mActionView == null) {
637            // We're already collapsed if we have no action view.
638            return true;
639        }
640
641        if (mOnActionExpandListener == null ||
642                mOnActionExpandListener.onMenuItemActionCollapse(this)) {
643            return mMenu.collapseItemActionView(this);
644        }
645
646        return false;
647    }
648
649    @Override
650    public MenuItem setOnActionExpandListener(OnActionExpandListener listener) {
651        mOnActionExpandListener = listener;
652        return this;
653    }
654
655    public boolean hasCollapsibleActionView() {
656        if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) != 0) {
657            if (mActionView == null && mActionProvider != null) {
658                mActionView = mActionProvider.onCreateActionView(this);
659            }
660            return mActionView != null;
661        }
662        return false;
663    }
664
665    public void setActionViewExpanded(boolean isExpanded) {
666        mIsActionViewExpanded = isExpanded;
667        mMenu.onItemsChanged(false);
668    }
669
670    public boolean isActionViewExpanded() {
671        return mIsActionViewExpanded;
672    }
673}
674