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