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