179b8f0379ee733c0fc0aac7eb1a48630cc5d862
[pub/Android/ownCloud.git] / actionbarsherlock / src / com / actionbarsherlock / internal / view / menu / MenuBuilder.java
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.actionbarsherlock.internal.view.menu;
18
19
20 import java.lang.ref.WeakReference;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.concurrent.CopyOnWriteArrayList;
25
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.content.res.Configuration;
32 import android.content.res.Resources;
33 import android.graphics.drawable.Drawable;
34 import android.os.Bundle;
35 import android.os.Parcelable;
36 import android.util.SparseArray;
37 import android.view.ContextMenu.ContextMenuInfo;
38 import android.view.KeyCharacterMap;
39 import android.view.KeyEvent;
40 import android.view.View;
41
42 import com.actionbarsherlock.R;
43 import com.actionbarsherlock.view.ActionProvider;
44 import com.actionbarsherlock.view.Menu;
45 import com.actionbarsherlock.view.MenuItem;
46 import com.actionbarsherlock.view.SubMenu;
47
48 /**
49 * Implementation of the {@link android.view.Menu} interface for creating a
50 * standard menu UI.
51 */
52 public class MenuBuilder implements Menu {
53 //UNUSED private static final String TAG = "MenuBuilder";
54
55 private static final String PRESENTER_KEY = "android:menu:presenters";
56 private static final String ACTION_VIEW_STATES_KEY = "android:menu:actionviewstates";
57 private static final String EXPANDED_ACTION_VIEW_ID = "android:menu:expandedactionview";
58
59 private static final int[] sCategoryToOrder = new int[] {
60 1, /* No category */
61 4, /* CONTAINER */
62 5, /* SYSTEM */
63 3, /* SECONDARY */
64 2, /* ALTERNATIVE */
65 0, /* SELECTED_ALTERNATIVE */
66 };
67
68 private final Context mContext;
69 private final Resources mResources;
70
71 /**
72 * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode()
73 * instead of accessing this directly.
74 */
75 private boolean mQwertyMode;
76
77 /**
78 * Whether the shortcuts should be visible on menus. Use isShortcutsVisible()
79 * instead of accessing this directly.
80 */
81 private boolean mShortcutsVisible;
82
83 /**
84 * Callback that will receive the various menu-related events generated by
85 * this class. Use getCallback to get a reference to the callback.
86 */
87 private Callback mCallback;
88
89 /** Contains all of the items for this menu */
90 private ArrayList<MenuItemImpl> mItems;
91
92 /** Contains only the items that are currently visible. This will be created/refreshed from
93 * {@link #getVisibleItems()} */
94 private ArrayList<MenuItemImpl> mVisibleItems;
95 /**
96 * Whether or not the items (or any one item's shown state) has changed since it was last
97 * fetched from {@link #getVisibleItems()}
98 */
99 private boolean mIsVisibleItemsStale;
100
101 /**
102 * Contains only the items that should appear in the Action Bar, if present.
103 */
104 private ArrayList<MenuItemImpl> mActionItems;
105 /**
106 * Contains items that should NOT appear in the Action Bar, if present.
107 */
108 private ArrayList<MenuItemImpl> mNonActionItems;
109
110 /**
111 * Whether or not the items (or any one item's action state) has changed since it was
112 * last fetched.
113 */
114 private boolean mIsActionItemsStale;
115
116 /**
117 * Default value for how added items should show in the action list.
118 */
119 private int mDefaultShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER;
120
121 /**
122 * Current use case is Context Menus: As Views populate the context menu, each one has
123 * extra information that should be passed along. This is the current menu info that
124 * should be set on all items added to this menu.
125 */
126 private ContextMenuInfo mCurrentMenuInfo;
127
128 /** Header title for menu types that have a header (context and submenus) */
129 CharSequence mHeaderTitle;
130 /** Header icon for menu types that have a header and support icons (context) */
131 Drawable mHeaderIcon;
132 /** Header custom view for menu types that have a header and support custom views (context) */
133 View mHeaderView;
134
135 /**
136 * Contains the state of the View hierarchy for all menu views when the menu
137 * was frozen.
138 */
139 //UNUSED private SparseArray<Parcelable> mFrozenViewStates;
140
141 /**
142 * Prevents onItemsChanged from doing its junk, useful for batching commands
143 * that may individually call onItemsChanged.
144 */
145 private boolean mPreventDispatchingItemsChanged = false;
146 private boolean mItemsChangedWhileDispatchPrevented = false;
147
148 private boolean mOptionalIconsVisible = false;
149
150 private boolean mIsClosing = false;
151
152 private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>();
153
154 private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters =
155 new CopyOnWriteArrayList<WeakReference<MenuPresenter>>();
156
157 /**
158 * Currently expanded menu item; must be collapsed when we clear.
159 */
160 private MenuItemImpl mExpandedItem;
161
162 /**
163 * Called by menu to notify of close and selection changes.
164 */
165 public interface Callback {
166 /**
167 * Called when a menu item is selected.
168 * @param menu The menu that is the parent of the item
169 * @param item The menu item that is selected
170 * @return whether the menu item selection was handled
171 */
172 public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item);
173
174 /**
175 * Called when the mode of the menu changes (for example, from icon to expanded).
176 *
177 * @param menu the menu that has changed modes
178 */
179 public void onMenuModeChange(MenuBuilder menu);
180 }
181
182 /**
183 * Called by menu items to execute their associated action
184 */
185 public interface ItemInvoker {
186 public boolean invokeItem(MenuItemImpl item);
187 }
188
189 public MenuBuilder(Context context) {
190 mContext = context;
191 mResources = context.getResources();
192
193 mItems = new ArrayList<MenuItemImpl>();
194
195 mVisibleItems = new ArrayList<MenuItemImpl>();
196 mIsVisibleItemsStale = true;
197
198 mActionItems = new ArrayList<MenuItemImpl>();
199 mNonActionItems = new ArrayList<MenuItemImpl>();
200 mIsActionItemsStale = true;
201
202 setShortcutsVisibleInner(true);
203 }
204
205 public MenuBuilder setDefaultShowAsAction(int defaultShowAsAction) {
206 mDefaultShowAsAction = defaultShowAsAction;
207 return this;
208 }
209
210 /**
211 * Add a presenter to this menu. This will only hold a WeakReference;
212 * you do not need to explicitly remove a presenter, but you can using
213 * {@link #removeMenuPresenter(MenuPresenter)}.
214 *
215 * @param presenter The presenter to add
216 */
217 public void addMenuPresenter(MenuPresenter presenter) {
218 mPresenters.add(new WeakReference<MenuPresenter>(presenter));
219 presenter.initForMenu(mContext, this);
220 mIsActionItemsStale = true;
221 }
222
223 /**
224 * Remove a presenter from this menu. That presenter will no longer
225 * receive notifications of updates to this menu's data.
226 *
227 * @param presenter The presenter to remove
228 */
229 public void removeMenuPresenter(MenuPresenter presenter) {
230 for (WeakReference<MenuPresenter> ref : mPresenters) {
231 final MenuPresenter item = ref.get();
232 if (item == null || item == presenter) {
233 mPresenters.remove(ref);
234 }
235 }
236 }
237
238 private void dispatchPresenterUpdate(boolean cleared) {
239 if (mPresenters.isEmpty()) return;
240
241 stopDispatchingItemsChanged();
242 for (WeakReference<MenuPresenter> ref : mPresenters) {
243 final MenuPresenter presenter = ref.get();
244 if (presenter == null) {
245 mPresenters.remove(ref);
246 } else {
247 presenter.updateMenuView(cleared);
248 }
249 }
250 startDispatchingItemsChanged();
251 }
252
253 private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu) {
254 if (mPresenters.isEmpty()) return false;
255
256 boolean result = false;
257
258 for (WeakReference<MenuPresenter> ref : mPresenters) {
259 final MenuPresenter presenter = ref.get();
260 if (presenter == null) {
261 mPresenters.remove(ref);
262 } else if (!result) {
263 result = presenter.onSubMenuSelected(subMenu);
264 }
265 }
266 return result;
267 }
268
269 private void dispatchSaveInstanceState(Bundle outState) {
270 if (mPresenters.isEmpty()) return;
271
272 SparseArray<Parcelable> presenterStates = new SparseArray<Parcelable>();
273
274 for (WeakReference<MenuPresenter> ref : mPresenters) {
275 final MenuPresenter presenter = ref.get();
276 if (presenter == null) {
277 mPresenters.remove(ref);
278 } else {
279 final int id = presenter.getId();
280 if (id > 0) {
281 final Parcelable state = presenter.onSaveInstanceState();
282 if (state != null) {
283 presenterStates.put(id, state);
284 }
285 }
286 }
287 }
288
289 outState.putSparseParcelableArray(PRESENTER_KEY, presenterStates);
290 }
291
292 private void dispatchRestoreInstanceState(Bundle state) {
293 SparseArray<Parcelable> presenterStates = state.getSparseParcelableArray(PRESENTER_KEY);
294
295 if (presenterStates == null || mPresenters.isEmpty()) return;
296
297 for (WeakReference<MenuPresenter> ref : mPresenters) {
298 final MenuPresenter presenter = ref.get();
299 if (presenter == null) {
300 mPresenters.remove(ref);
301 } else {
302 final int id = presenter.getId();
303 if (id > 0) {
304 Parcelable parcel = presenterStates.get(id);
305 if (parcel != null) {
306 presenter.onRestoreInstanceState(parcel);
307 }
308 }
309 }
310 }
311 }
312
313 public void savePresenterStates(Bundle outState) {
314 dispatchSaveInstanceState(outState);
315 }
316
317 public void restorePresenterStates(Bundle state) {
318 dispatchRestoreInstanceState(state);
319 }
320
321 public void saveActionViewStates(Bundle outStates) {
322 SparseArray<Parcelable> viewStates = null;
323
324 final int itemCount = size();
325 for (int i = 0; i < itemCount; i++) {
326 final MenuItem item = getItem(i);
327 final View v = item.getActionView();
328 if (v != null && v.getId() != View.NO_ID) {
329 if (viewStates == null) {
330 viewStates = new SparseArray<Parcelable>();
331 }
332 v.saveHierarchyState(viewStates);
333 if (item.isActionViewExpanded()) {
334 outStates.putInt(EXPANDED_ACTION_VIEW_ID, item.getItemId());
335 }
336 }
337 if (item.hasSubMenu()) {
338 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
339 subMenu.saveActionViewStates(outStates);
340 }
341 }
342
343 if (viewStates != null) {
344 outStates.putSparseParcelableArray(getActionViewStatesKey(), viewStates);
345 }
346 }
347
348 public void restoreActionViewStates(Bundle states) {
349 if (states == null) {
350 return;
351 }
352
353 SparseArray<Parcelable> viewStates = states.getSparseParcelableArray(
354 getActionViewStatesKey());
355
356 final int itemCount = size();
357 for (int i = 0; i < itemCount; i++) {
358 final MenuItem item = getItem(i);
359 final View v = item.getActionView();
360 if (v != null && v.getId() != View.NO_ID) {
361 v.restoreHierarchyState(viewStates);
362 }
363 if (item.hasSubMenu()) {
364 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
365 subMenu.restoreActionViewStates(states);
366 }
367 }
368
369 final int expandedId = states.getInt(EXPANDED_ACTION_VIEW_ID);
370 if (expandedId > 0) {
371 MenuItem itemToExpand = findItem(expandedId);
372 if (itemToExpand != null) {
373 itemToExpand.expandActionView();
374 }
375 }
376 }
377
378 protected String getActionViewStatesKey() {
379 return ACTION_VIEW_STATES_KEY;
380 }
381
382 public void setCallback(Callback cb) {
383 mCallback = cb;
384 }
385
386 /**
387 * Adds an item to the menu. The other add methods funnel to this.
388 */
389 private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
390 final int ordering = getOrdering(categoryOrder);
391
392 final MenuItemImpl item = new MenuItemImpl(this, group, id, categoryOrder,
393 ordering, title, mDefaultShowAsAction);
394
395 if (mCurrentMenuInfo != null) {
396 // Pass along the current menu info
397 item.setMenuInfo(mCurrentMenuInfo);
398 }
399
400 mItems.add(findInsertIndex(mItems, ordering), item);
401 onItemsChanged(true);
402
403 return item;
404 }
405
406 public MenuItem add(CharSequence title) {
407 return addInternal(0, 0, 0, title);
408 }
409
410 public MenuItem add(int titleRes) {
411 return addInternal(0, 0, 0, mResources.getString(titleRes));
412 }
413
414 public MenuItem add(int group, int id, int categoryOrder, CharSequence title) {
415 return addInternal(group, id, categoryOrder, title);
416 }
417
418 public MenuItem add(int group, int id, int categoryOrder, int title) {
419 return addInternal(group, id, categoryOrder, mResources.getString(title));
420 }
421
422 public SubMenu addSubMenu(CharSequence title) {
423 return addSubMenu(0, 0, 0, title);
424 }
425
426 public SubMenu addSubMenu(int titleRes) {
427 return addSubMenu(0, 0, 0, mResources.getString(titleRes));
428 }
429
430 public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
431 final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title);
432 final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item);
433 item.setSubMenu(subMenu);
434
435 return subMenu;
436 }
437
438 public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) {
439 return addSubMenu(group, id, categoryOrder, mResources.getString(title));
440 }
441
442 public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller,
443 Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) {
444 PackageManager pm = mContext.getPackageManager();
445 final List<ResolveInfo> lri =
446 pm.queryIntentActivityOptions(caller, specifics, intent, 0);
447 final int N = lri != null ? lri.size() : 0;
448
449 if ((flags & FLAG_APPEND_TO_GROUP) == 0) {
450 removeGroup(group);
451 }
452
453 for (int i=0; i<N; i++) {
454 final ResolveInfo ri = lri.get(i);
455 Intent rintent = new Intent(
456 ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]);
457 rintent.setComponent(new ComponentName(
458 ri.activityInfo.applicationInfo.packageName,
459 ri.activityInfo.name));
460 final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm))
461 .setIcon(ri.loadIcon(pm))
462 .setIntent(rintent);
463 if (outSpecificItems != null && ri.specificIndex >= 0) {
464 outSpecificItems[ri.specificIndex] = item;
465 }
466 }
467
468 return N;
469 }
470
471 public void removeItem(int id) {
472 removeItemAtInt(findItemIndex(id), true);
473 }
474
475 public void removeGroup(int group) {
476 final int i = findGroupIndex(group);
477
478 if (i >= 0) {
479 final int maxRemovable = mItems.size() - i;
480 int numRemoved = 0;
481 while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) {
482 // Don't force update for each one, this method will do it at the end
483 removeItemAtInt(i, false);
484 }
485
486 // Notify menu views
487 onItemsChanged(true);
488 }
489 }
490
491 /**
492 * Remove the item at the given index and optionally forces menu views to
493 * update.
494 *
495 * @param index The index of the item to be removed. If this index is
496 * invalid an exception is thrown.
497 * @param updateChildrenOnMenuViews Whether to force update on menu views.
498 * Please make sure you eventually call this after your batch of
499 * removals.
500 */
501 private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) {
502 if ((index < 0) || (index >= mItems.size())) return;
503
504 mItems.remove(index);
505
506 if (updateChildrenOnMenuViews) onItemsChanged(true);
507 }
508
509 public void removeItemAt(int index) {
510 removeItemAtInt(index, true);
511 }
512
513 public void clearAll() {
514 mPreventDispatchingItemsChanged = true;
515 clear();
516 clearHeader();
517 mPreventDispatchingItemsChanged = false;
518 mItemsChangedWhileDispatchPrevented = false;
519 onItemsChanged(true);
520 }
521
522 public void clear() {
523 if (mExpandedItem != null) {
524 collapseItemActionView(mExpandedItem);
525 }
526 mItems.clear();
527
528 onItemsChanged(true);
529 }
530
531 void setExclusiveItemChecked(MenuItem item) {
532 final int group = item.getGroupId();
533
534 final int N = mItems.size();
535 for (int i = 0; i < N; i++) {
536 MenuItemImpl curItem = mItems.get(i);
537 if (curItem.getGroupId() == group) {
538 if (!curItem.isExclusiveCheckable()) continue;
539 if (!curItem.isCheckable()) continue;
540
541 // Check the item meant to be checked, uncheck the others (that are in the group)
542 curItem.setCheckedInt(curItem == item);
543 }
544 }
545 }
546
547 public void setGroupCheckable(int group, boolean checkable, boolean exclusive) {
548 final int N = mItems.size();
549
550 for (int i = 0; i < N; i++) {
551 MenuItemImpl item = mItems.get(i);
552 if (item.getGroupId() == group) {
553 item.setExclusiveCheckable(exclusive);
554 item.setCheckable(checkable);
555 }
556 }
557 }
558
559 public void setGroupVisible(int group, boolean visible) {
560 final int N = mItems.size();
561
562 // We handle the notification of items being changed ourselves, so we use setVisibleInt rather
563 // than setVisible and at the end notify of items being changed
564
565 boolean changedAtLeastOneItem = false;
566 for (int i = 0; i < N; i++) {
567 MenuItemImpl item = mItems.get(i);
568 if (item.getGroupId() == group) {
569 if (item.setVisibleInt(visible)) changedAtLeastOneItem = true;
570 }
571 }
572
573 if (changedAtLeastOneItem) onItemsChanged(true);
574 }
575
576 public void setGroupEnabled(int group, boolean enabled) {
577 final int N = mItems.size();
578
579 for (int i = 0; i < N; i++) {
580 MenuItemImpl item = mItems.get(i);
581 if (item.getGroupId() == group) {
582 item.setEnabled(enabled);
583 }
584 }
585 }
586
587 public boolean hasVisibleItems() {
588 final int size = size();
589
590 for (int i = 0; i < size; i++) {
591 MenuItemImpl item = mItems.get(i);
592 if (item.isVisible()) {
593 return true;
594 }
595 }
596
597 return false;
598 }
599
600 public MenuItem findItem(int id) {
601 final int size = size();
602 for (int i = 0; i < size; i++) {
603 MenuItemImpl item = mItems.get(i);
604 if (item.getItemId() == id) {
605 return item;
606 } else if (item.hasSubMenu()) {
607 MenuItem possibleItem = item.getSubMenu().findItem(id);
608
609 if (possibleItem != null) {
610 return possibleItem;
611 }
612 }
613 }
614
615 return null;
616 }
617
618 public int findItemIndex(int id) {
619 final int size = size();
620
621 for (int i = 0; i < size; i++) {
622 MenuItemImpl item = mItems.get(i);
623 if (item.getItemId() == id) {
624 return i;
625 }
626 }
627
628 return -1;
629 }
630
631 public int findGroupIndex(int group) {
632 return findGroupIndex(group, 0);
633 }
634
635 public int findGroupIndex(int group, int start) {
636 final int size = size();
637
638 if (start < 0) {
639 start = 0;
640 }
641
642 for (int i = start; i < size; i++) {
643 final MenuItemImpl item = mItems.get(i);
644
645 if (item.getGroupId() == group) {
646 return i;
647 }
648 }
649
650 return -1;
651 }
652
653 public int size() {
654 return mItems.size();
655 }
656
657 /** {@inheritDoc} */
658 public MenuItem getItem(int index) {
659 return mItems.get(index);
660 }
661
662 public boolean isShortcutKey(int keyCode, KeyEvent event) {
663 return findItemWithShortcutForKey(keyCode, event) != null;
664 }
665
666 public void setQwertyMode(boolean isQwerty) {
667 mQwertyMode = isQwerty;
668
669 onItemsChanged(false);
670 }
671
672 /**
673 * Returns the ordering across all items. This will grab the category from
674 * the upper bits, find out how to order the category with respect to other
675 * categories, and combine it with the lower bits.
676 *
677 * @param categoryOrder The category order for a particular item (if it has
678 * not been or/add with a category, the default category is
679 * assumed).
680 * @return An ordering integer that can be used to order this item across
681 * all the items (even from other categories).
682 */
683 private static int getOrdering(int categoryOrder) {
684 final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT;
685
686 if (index < 0 || index >= sCategoryToOrder.length) {
687 throw new IllegalArgumentException("order does not contain a valid category.");
688 }
689
690 return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK);
691 }
692
693 /**
694 * @return whether the menu shortcuts are in qwerty mode or not
695 */
696 boolean isQwertyMode() {
697 return mQwertyMode;
698 }
699
700 /**
701 * Sets whether the shortcuts should be visible on menus. Devices without hardware
702 * key input will never make shortcuts visible even if this method is passed 'true'.
703 *
704 * @param shortcutsVisible Whether shortcuts should be visible (if true and a
705 * menu item does not have a shortcut defined, that item will
706 * still NOT show a shortcut)
707 */
708 public void setShortcutsVisible(boolean shortcutsVisible) {
709 if (mShortcutsVisible == shortcutsVisible) return;
710
711 setShortcutsVisibleInner(shortcutsVisible);
712 onItemsChanged(false);
713 }
714
715 private void setShortcutsVisibleInner(boolean shortcutsVisible) {
716 mShortcutsVisible = shortcutsVisible
717 && mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS
718 && mResources.getBoolean(
719 R.bool.abs__config_showMenuShortcutsWhenKeyboardPresent);
720 }
721
722 /**
723 * @return Whether shortcuts should be visible on menus.
724 */
725 public boolean isShortcutsVisible() {
726 return mShortcutsVisible;
727 }
728
729 Resources getResources() {
730 return mResources;
731 }
732
733 public Context getContext() {
734 return mContext;
735 }
736
737 boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) {
738 return mCallback != null && mCallback.onMenuItemSelected(menu, item);
739 }
740
741 /**
742 * Dispatch a mode change event to this menu's callback.
743 */
744 public void changeMenuMode() {
745 if (mCallback != null) {
746 mCallback.onMenuModeChange(this);
747 }
748 }
749
750 private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) {
751 for (int i = items.size() - 1; i >= 0; i--) {
752 MenuItemImpl item = items.get(i);
753 if (item.getOrdering() <= ordering) {
754 return i + 1;
755 }
756 }
757
758 return 0;
759 }
760
761 public boolean performShortcut(int keyCode, KeyEvent event, int flags) {
762 final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event);
763
764 boolean handled = false;
765
766 if (item != null) {
767 handled = performItemAction(item, flags);
768 }
769
770 if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) {
771 close(true);
772 }
773
774 return handled;
775 }
776
777 /*
778 * This function will return all the menu and sub-menu items that can
779 * be directly (the shortcut directly corresponds) and indirectly
780 * (the ALT-enabled char corresponds to the shortcut) associated
781 * with the keyCode.
782 */
783 @SuppressWarnings("deprecation")
784 void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) {
785 final boolean qwerty = isQwertyMode();
786 final int metaState = event.getMetaState();
787 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
788 // Get the chars associated with the keyCode (i.e using any chording combo)
789 final boolean isKeyCodeMapped = event.getKeyData(possibleChars);
790 // The delete key is not mapped to '\b' so we treat it specially
791 if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) {
792 return;
793 }
794
795 // Look for an item whose shortcut is this key.
796 final int N = mItems.size();
797 for (int i = 0; i < N; i++) {
798 MenuItemImpl item = mItems.get(i);
799 if (item.hasSubMenu()) {
800 ((MenuBuilder)item.getSubMenu()).findItemsWithShortcutForKey(items, keyCode, event);
801 }
802 final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut();
803 if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) &&
804 (shortcutChar != 0) &&
805 (shortcutChar == possibleChars.meta[0]
806 || shortcutChar == possibleChars.meta[2]
807 || (qwerty && shortcutChar == '\b' &&
808 keyCode == KeyEvent.KEYCODE_DEL)) &&
809 item.isEnabled()) {
810 items.add(item);
811 }
812 }
813 }
814
815 /*
816 * We want to return the menu item associated with the key, but if there is no
817 * ambiguity (i.e. there is only one menu item corresponding to the key) we want
818 * to return it even if it's not an exact match; this allow the user to
819 * _not_ use the ALT key for example, making the use of shortcuts slightly more
820 * user-friendly. An example is on the G1, '!' and '1' are on the same key, and
821 * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut).
822 *
823 * On the other hand, if two (or more) shortcuts corresponds to the same key,
824 * we have to only return the exact match.
825 */
826 @SuppressWarnings("deprecation")
827 MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) {
828 // Get all items that can be associated directly or indirectly with the keyCode
829 ArrayList<MenuItemImpl> items = mTempShortcutItemList;
830 items.clear();
831 findItemsWithShortcutForKey(items, keyCode, event);
832
833 if (items.isEmpty()) {
834 return null;
835 }
836
837 final int metaState = event.getMetaState();
838 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData();
839 // Get the chars associated with the keyCode (i.e using any chording combo)
840 event.getKeyData(possibleChars);
841
842 // If we have only one element, we can safely returns it
843 final int size = items.size();
844 if (size == 1) {
845 return items.get(0);
846 }
847
848 final boolean qwerty = isQwertyMode();
849 // If we found more than one item associated with the key,
850 // we have to return the exact match
851 for (int i = 0; i < size; i++) {
852 final MenuItemImpl item = items.get(i);
853 final char shortcutChar = qwerty ? item.getAlphabeticShortcut() :
854 item.getNumericShortcut();
855 if ((shortcutChar == possibleChars.meta[0] &&
856 (metaState & KeyEvent.META_ALT_ON) == 0)
857 || (shortcutChar == possibleChars.meta[2] &&
858 (metaState & KeyEvent.META_ALT_ON) != 0)
859 || (qwerty && shortcutChar == '\b' &&
860 keyCode == KeyEvent.KEYCODE_DEL)) {
861 return item;
862 }
863 }
864 return null;
865 }
866
867 public boolean performIdentifierAction(int id, int flags) {
868 // Look for an item whose identifier is the id.
869 return performItemAction(findItem(id), flags);
870 }
871
872 public boolean performItemAction(MenuItem item, int flags) {
873 MenuItemImpl itemImpl = (MenuItemImpl) item;
874
875 if (itemImpl == null || !itemImpl.isEnabled()) {
876 return false;
877 }
878
879 boolean invoked = itemImpl.invoke();
880
881 if (itemImpl.hasCollapsibleActionView()) {
882 invoked |= itemImpl.expandActionView();
883 if (invoked) close(true);
884 } else if (item.hasSubMenu()) {
885 close(false);
886
887 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
888 final ActionProvider provider = item.getActionProvider();
889 if (provider != null && provider.hasSubMenu()) {
890 provider.onPrepareSubMenu(subMenu);
891 }
892 invoked |= dispatchSubMenuSelected(subMenu);
893 if (!invoked) close(true);
894 } else {
895 if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) {
896 close(true);
897 }
898 }
899
900 return invoked;
901 }
902
903 /**
904 * Closes the visible menu.
905 *
906 * @param allMenusAreClosing Whether the menus are completely closing (true),
907 * or whether there is another menu coming in this menu's place
908 * (false). For example, if the menu is closing because a
909 * sub menu is about to be shown, <var>allMenusAreClosing</var>
910 * is false.
911 */
912 final void close(boolean allMenusAreClosing) {
913 if (mIsClosing) return;
914
915 mIsClosing = true;
916 for (WeakReference<MenuPresenter> ref : mPresenters) {
917 final MenuPresenter presenter = ref.get();
918 if (presenter == null) {
919 mPresenters.remove(ref);
920 } else {
921 presenter.onCloseMenu(this, allMenusAreClosing);
922 }
923 }
924 mIsClosing = false;
925 }
926
927 /** {@inheritDoc} */
928 public void close() {
929 close(true);
930 }
931
932 /**
933 * Called when an item is added or removed.
934 *
935 * @param structureChanged true if the menu structure changed,
936 * false if only item properties changed.
937 * (Visibility is a structural property since it affects layout.)
938 */
939 void onItemsChanged(boolean structureChanged) {
940 if (!mPreventDispatchingItemsChanged) {
941 if (structureChanged) {
942 mIsVisibleItemsStale = true;
943 mIsActionItemsStale = true;
944 }
945
946 dispatchPresenterUpdate(structureChanged);
947 } else {
948 mItemsChangedWhileDispatchPrevented = true;
949 }
950 }
951
952 /**
953 * Stop dispatching item changed events to presenters until
954 * {@link #startDispatchingItemsChanged()} is called. Useful when
955 * many menu operations are going to be performed as a batch.
956 */
957 public void stopDispatchingItemsChanged() {
958 if (!mPreventDispatchingItemsChanged) {
959 mPreventDispatchingItemsChanged = true;
960 mItemsChangedWhileDispatchPrevented = false;
961 }
962 }
963
964 public void startDispatchingItemsChanged() {
965 mPreventDispatchingItemsChanged = false;
966
967 if (mItemsChangedWhileDispatchPrevented) {
968 mItemsChangedWhileDispatchPrevented = false;
969 onItemsChanged(true);
970 }
971 }
972
973 /**
974 * Called by {@link MenuItemImpl} when its visible flag is changed.
975 * @param item The item that has gone through a visibility change.
976 */
977 void onItemVisibleChanged(MenuItemImpl item) {
978 // Notify of items being changed
979 mIsVisibleItemsStale = true;
980 onItemsChanged(true);
981 }
982
983 /**
984 * Called by {@link MenuItemImpl} when its action request status is changed.
985 * @param item The item that has gone through a change in action request status.
986 */
987 void onItemActionRequestChanged(MenuItemImpl item) {
988 // Notify of items being changed
989 mIsActionItemsStale = true;
990 onItemsChanged(true);
991 }
992
993 ArrayList<MenuItemImpl> getVisibleItems() {
994 if (!mIsVisibleItemsStale) return mVisibleItems;
995
996 // Refresh the visible items
997 mVisibleItems.clear();
998
999 final int itemsSize = mItems.size();
1000 MenuItemImpl item;
1001 for (int i = 0; i < itemsSize; i++) {
1002 item = mItems.get(i);
1003 if (item.isVisible()) mVisibleItems.add(item);
1004 }
1005
1006 mIsVisibleItemsStale = false;
1007 mIsActionItemsStale = true;
1008
1009 return mVisibleItems;
1010 }
1011
1012 /**
1013 * This method determines which menu items get to be 'action items' that will appear
1014 * in an action bar and which items should be 'overflow items' in a secondary menu.
1015 * The rules are as follows:
1016 *
1017 * <p>Items are considered for inclusion in the order specified within the menu.
1018 * There is a limit of mMaxActionItems as a total count, optionally including the overflow
1019 * menu button itself. This is a soft limit; if an item shares a group ID with an item
1020 * previously included as an action item, the new item will stay with its group and become
1021 * an action item itself even if it breaks the max item count limit. This is done to
1022 * limit the conceptual complexity of the items presented within an action bar. Only a few
1023 * unrelated concepts should be presented to the user in this space, and groups are treated
1024 * as a single concept.
1025 *
1026 * <p>There is also a hard limit of consumed measurable space: mActionWidthLimit. This
1027 * limit may be broken by a single item that exceeds the remaining space, but no further
1028 * items may be added. If an item that is part of a group cannot fit within the remaining
1029 * measured width, the entire group will be demoted to overflow. This is done to ensure room
1030 * for navigation and other affordances in the action bar as well as reduce general UI clutter.
1031 *
1032 * <p>The space freed by demoting a full group cannot be consumed by future menu items.
1033 * Once items begin to overflow, all future items become overflow items as well. This is
1034 * to avoid inadvertent reordering that may break the app's intended design.
1035 */
1036 public void flagActionItems() {
1037 if (!mIsActionItemsStale) {
1038 return;
1039 }
1040
1041 // Presenters flag action items as needed.
1042 boolean flagged = false;
1043 for (WeakReference<MenuPresenter> ref : mPresenters) {
1044 final MenuPresenter presenter = ref.get();
1045 if (presenter == null) {
1046 mPresenters.remove(ref);
1047 } else {
1048 flagged |= presenter.flagActionItems();
1049 }
1050 }
1051
1052 if (flagged) {
1053 mActionItems.clear();
1054 mNonActionItems.clear();
1055 ArrayList<MenuItemImpl> visibleItems = getVisibleItems();
1056 final int itemsSize = visibleItems.size();
1057 for (int i = 0; i < itemsSize; i++) {
1058 MenuItemImpl item = visibleItems.get(i);
1059 if (item.isActionButton()) {
1060 mActionItems.add(item);
1061 } else {
1062 mNonActionItems.add(item);
1063 }
1064 }
1065 } else {
1066 // Nobody flagged anything, everything is a non-action item.
1067 // (This happens during a first pass with no action-item presenters.)
1068 mActionItems.clear();
1069 mNonActionItems.clear();
1070 mNonActionItems.addAll(getVisibleItems());
1071 }
1072 mIsActionItemsStale = false;
1073 }
1074
1075 ArrayList<MenuItemImpl> getActionItems() {
1076 flagActionItems();
1077 return mActionItems;
1078 }
1079
1080 ArrayList<MenuItemImpl> getNonActionItems() {
1081 flagActionItems();
1082 return mNonActionItems;
1083 }
1084
1085 public void clearHeader() {
1086 mHeaderIcon = null;
1087 mHeaderTitle = null;
1088 mHeaderView = null;
1089
1090 onItemsChanged(false);
1091 }
1092
1093 private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes,
1094 final Drawable icon, final View view) {
1095 final Resources r = getResources();
1096
1097 if (view != null) {
1098 mHeaderView = view;
1099
1100 // If using a custom view, then the title and icon aren't used
1101 mHeaderTitle = null;
1102 mHeaderIcon = null;
1103 } else {
1104 if (titleRes > 0) {
1105 mHeaderTitle = r.getText(titleRes);
1106 } else if (title != null) {
1107 mHeaderTitle = title;
1108 }
1109
1110 if (iconRes > 0) {
1111 mHeaderIcon = r.getDrawable(iconRes);
1112 } else if (icon != null) {
1113 mHeaderIcon = icon;
1114 }
1115
1116 // If using the title or icon, then a custom view isn't used
1117 mHeaderView = null;
1118 }
1119
1120 // Notify of change
1121 onItemsChanged(false);
1122 }
1123
1124 /**
1125 * Sets the header's title. This replaces the header view. Called by the
1126 * builder-style methods of subclasses.
1127 *
1128 * @param title The new title.
1129 * @return This MenuBuilder so additional setters can be called.
1130 */
1131 protected MenuBuilder setHeaderTitleInt(CharSequence title) {
1132 setHeaderInternal(0, title, 0, null, null);
1133 return this;
1134 }
1135
1136 /**
1137 * Sets the header's title. This replaces the header view. Called by the
1138 * builder-style methods of subclasses.
1139 *
1140 * @param titleRes The new title (as a resource ID).
1141 * @return This MenuBuilder so additional setters can be called.
1142 */
1143 protected MenuBuilder setHeaderTitleInt(int titleRes) {
1144 setHeaderInternal(titleRes, null, 0, null, null);
1145 return this;
1146 }
1147
1148 /**
1149 * Sets the header's icon. This replaces the header view. Called by the
1150 * builder-style methods of subclasses.
1151 *
1152 * @param icon The new icon.
1153 * @return This MenuBuilder so additional setters can be called.
1154 */
1155 protected MenuBuilder setHeaderIconInt(Drawable icon) {
1156 setHeaderInternal(0, null, 0, icon, null);
1157 return this;
1158 }
1159
1160 /**
1161 * Sets the header's icon. This replaces the header view. Called by the
1162 * builder-style methods of subclasses.
1163 *
1164 * @param iconRes The new icon (as a resource ID).
1165 * @return This MenuBuilder so additional setters can be called.
1166 */
1167 protected MenuBuilder setHeaderIconInt(int iconRes) {
1168 setHeaderInternal(0, null, iconRes, null, null);
1169 return this;
1170 }
1171
1172 /**
1173 * Sets the header's view. This replaces the title and icon. Called by the
1174 * builder-style methods of subclasses.
1175 *
1176 * @param view The new view.
1177 * @return This MenuBuilder so additional setters can be called.
1178 */
1179 protected MenuBuilder setHeaderViewInt(View view) {
1180 setHeaderInternal(0, null, 0, null, view);
1181 return this;
1182 }
1183
1184 public CharSequence getHeaderTitle() {
1185 return mHeaderTitle;
1186 }
1187
1188 public Drawable getHeaderIcon() {
1189 return mHeaderIcon;
1190 }
1191
1192 public View getHeaderView() {
1193 return mHeaderView;
1194 }
1195
1196 /**
1197 * Gets the root menu (if this is a submenu, find its root menu).
1198 * @return The root menu.
1199 */
1200 public MenuBuilder getRootMenu() {
1201 return this;
1202 }
1203
1204 /**
1205 * Sets the current menu info that is set on all items added to this menu
1206 * (until this is called again with different menu info, in which case that
1207 * one will be added to all subsequent item additions).
1208 *
1209 * @param menuInfo The extra menu information to add.
1210 */
1211 public void setCurrentMenuInfo(ContextMenuInfo menuInfo) {
1212 mCurrentMenuInfo = menuInfo;
1213 }
1214
1215 void setOptionalIconsVisible(boolean visible) {
1216 mOptionalIconsVisible = visible;
1217 }
1218
1219 boolean getOptionalIconsVisible() {
1220 return mOptionalIconsVisible;
1221 }
1222
1223 public boolean expandItemActionView(MenuItemImpl item) {
1224 if (mPresenters.isEmpty()) return false;
1225
1226 boolean expanded = false;
1227
1228 stopDispatchingItemsChanged();
1229 for (WeakReference<MenuPresenter> ref : mPresenters) {
1230 final MenuPresenter presenter = ref.get();
1231 if (presenter == null) {
1232 mPresenters.remove(ref);
1233 } else if ((expanded = presenter.expandItemActionView(this, item))) {
1234 break;
1235 }
1236 }
1237 startDispatchingItemsChanged();
1238
1239 if (expanded) {
1240 mExpandedItem = item;
1241 }
1242 return expanded;
1243 }
1244
1245 public boolean collapseItemActionView(MenuItemImpl item) {
1246 if (mPresenters.isEmpty() || mExpandedItem != item) return false;
1247
1248 boolean collapsed = false;
1249
1250 stopDispatchingItemsChanged();
1251 for (WeakReference<MenuPresenter> ref : mPresenters) {
1252 final MenuPresenter presenter = ref.get();
1253 if (presenter == null) {
1254 mPresenters.remove(ref);
1255 } else if ((collapsed = presenter.collapseItemActionView(this, item))) {
1256 break;
1257 }
1258 }
1259 startDispatchingItemsChanged();
1260
1261 if (collapsed) {
1262 mExpandedItem = null;
1263 }
1264 return collapsed;
1265 }
1266
1267 public MenuItemImpl getExpandedItem() {
1268 return mExpandedItem;
1269 }
1270
1271 public boolean bindNativeOverflow(android.view.Menu menu, android.view.MenuItem.OnMenuItemClickListener listener, HashMap<android.view.MenuItem, MenuItemImpl> map) {
1272 final List<MenuItemImpl> nonActionItems = getNonActionItems();
1273 if (nonActionItems == null || nonActionItems.size() == 0) {
1274 return false;
1275 }
1276
1277 boolean visible = false;
1278 menu.clear();
1279 for (MenuItemImpl nonActionItem : nonActionItems) {
1280 if (!nonActionItem.isVisible()) {
1281 continue;
1282 }
1283 visible = true;
1284
1285 android.view.MenuItem nativeItem;
1286 if (nonActionItem.hasSubMenu()) {
1287 android.view.SubMenu nativeSub = menu.addSubMenu(nonActionItem.getGroupId(), nonActionItem.getItemId(),
1288 nonActionItem.getOrder(), nonActionItem.getTitle());
1289
1290 SubMenuBuilder subMenu = (SubMenuBuilder)nonActionItem.getSubMenu();
1291 for (MenuItemImpl subItem : subMenu.getVisibleItems()) {
1292 android.view.MenuItem nativeSubItem = nativeSub.add(subItem.getGroupId(), subItem.getItemId(),
1293 subItem.getOrder(), subItem.getTitle());
1294
1295 nativeSubItem.setIcon(subItem.getIcon());
1296 nativeSubItem.setOnMenuItemClickListener(listener);
1297 nativeSubItem.setEnabled(subItem.isEnabled());
1298 nativeSubItem.setIntent(subItem.getIntent());
1299 nativeSubItem.setNumericShortcut(subItem.getNumericShortcut());
1300 nativeSubItem.setAlphabeticShortcut(subItem.getAlphabeticShortcut());
1301 nativeSubItem.setTitleCondensed(subItem.getTitleCondensed());
1302 nativeSubItem.setCheckable(subItem.isCheckable());
1303 nativeSubItem.setChecked(subItem.isChecked());
1304
1305 if (subItem.isExclusiveCheckable()) {
1306 nativeSub.setGroupCheckable(subItem.getGroupId(), true, true);
1307 }
1308
1309 map.put(nativeSubItem, subItem);
1310 }
1311
1312 nativeItem = nativeSub.getItem();
1313 } else {
1314 nativeItem = menu.add(nonActionItem.getGroupId(), nonActionItem.getItemId(),
1315 nonActionItem.getOrder(), nonActionItem.getTitle());
1316 }
1317 nativeItem.setIcon(nonActionItem.getIcon());
1318 nativeItem.setOnMenuItemClickListener(listener);
1319 nativeItem.setEnabled(nonActionItem.isEnabled());
1320 nativeItem.setIntent(nonActionItem.getIntent());
1321 nativeItem.setNumericShortcut(nonActionItem.getNumericShortcut());
1322 nativeItem.setAlphabeticShortcut(nonActionItem.getAlphabeticShortcut());
1323 nativeItem.setTitleCondensed(nonActionItem.getTitleCondensed());
1324 nativeItem.setCheckable(nonActionItem.isCheckable());
1325 nativeItem.setChecked(nonActionItem.isChecked());
1326
1327 if (nonActionItem.isExclusiveCheckable()) {
1328 menu.setGroupCheckable(nonActionItem.getGroupId(), true, true);
1329 }
1330
1331 map.put(nativeItem, nonActionItem);
1332 }
1333 return visible;
1334 }
1335 }