5a0f40859b96afdf68215be73ffcb104075bff6c
[pub/Android/ownCloud.git] / actionbarsherlock / src / com / actionbarsherlock / view / MenuInflater.java
1 /*
2 * Copyright (C) 2006 The Android Open Source Project
3 * 2011 Jake Wharton
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package com.actionbarsherlock.view;
19
20 import java.io.IOException;
21 import java.lang.reflect.Constructor;
22 import java.lang.reflect.Method;
23 import org.xmlpull.v1.XmlPullParser;
24 import org.xmlpull.v1.XmlPullParserException;
25 import android.content.Context;
26 import android.content.res.TypedArray;
27 import android.content.res.XmlResourceParser;
28 import android.util.AttributeSet;
29 import android.util.Log;
30 import android.util.TypedValue;
31 import android.util.Xml;
32 import android.view.InflateException;
33 import android.view.View;
34
35 import com.actionbarsherlock.R;
36 import com.actionbarsherlock.internal.view.menu.MenuItemImpl;
37
38 /**
39 * This class is used to instantiate menu XML files into Menu objects.
40 * <p>
41 * For performance reasons, menu inflation relies heavily on pre-processing of
42 * XML files that is done at build time. Therefore, it is not currently possible
43 * to use MenuInflater with an XmlPullParser over a plain XML file at runtime;
44 * it only works with an XmlPullParser returned from a compiled resource (R.
45 * <em>something</em> file.)
46 */
47 public class MenuInflater {
48 private static final String LOG_TAG = "MenuInflater";
49
50 /** Menu tag name in XML. */
51 private static final String XML_MENU = "menu";
52
53 /** Group tag name in XML. */
54 private static final String XML_GROUP = "group";
55
56 /** Item tag name in XML. */
57 private static final String XML_ITEM = "item";
58
59 private static final int NO_ID = 0;
60
61 private static final Class<?>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] {Context.class};
62
63 private static final Class<?>[] ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE = ACTION_VIEW_CONSTRUCTOR_SIGNATURE;
64
65 private final Object[] mActionViewConstructorArguments;
66
67 private final Object[] mActionProviderConstructorArguments;
68
69 private Context mContext;
70 private Object mRealOwner;
71
72 /**
73 * Constructs a menu inflater.
74 *
75 * @see Activity#getMenuInflater()
76 */
77 public MenuInflater(Context context) {
78 mContext = context;
79 mRealOwner = context;
80 mActionViewConstructorArguments = new Object[] {context};
81 mActionProviderConstructorArguments = mActionViewConstructorArguments;
82 }
83
84 /**
85 * Constructs a menu inflater.
86 *
87 * @see Activity#getMenuInflater()
88 * @hide
89 */
90 public MenuInflater(Context context, Object realOwner) {
91 mContext = context;
92 mRealOwner = realOwner;
93 mActionViewConstructorArguments = new Object[] {context};
94 mActionProviderConstructorArguments = mActionViewConstructorArguments;
95 }
96
97 /**
98 * Inflate a menu hierarchy from the specified XML resource. Throws
99 * {@link InflateException} if there is an error.
100 *
101 * @param menuRes Resource ID for an XML layout resource to load (e.g.,
102 * <code>R.menu.main_activity</code>)
103 * @param menu The Menu to inflate into. The items and submenus will be
104 * added to this Menu.
105 */
106 public void inflate(int menuRes, Menu menu) {
107 XmlResourceParser parser = null;
108 try {
109 parser = mContext.getResources().getLayout(menuRes);
110 AttributeSet attrs = Xml.asAttributeSet(parser);
111
112 parseMenu(parser, attrs, menu);
113 } catch (XmlPullParserException e) {
114 throw new InflateException("Error inflating menu XML", e);
115 } catch (IOException e) {
116 throw new InflateException("Error inflating menu XML", e);
117 } finally {
118 if (parser != null) parser.close();
119 }
120 }
121
122 /**
123 * Called internally to fill the given menu. If a sub menu is seen, it will
124 * call this recursively.
125 */
126 private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)
127 throws XmlPullParserException, IOException {
128 MenuState menuState = new MenuState(menu);
129
130 int eventType = parser.getEventType();
131 String tagName;
132 boolean lookingForEndOfUnknownTag = false;
133 String unknownTagName = null;
134
135 // This loop will skip to the menu start tag
136 do {
137 if (eventType == XmlPullParser.START_TAG) {
138 tagName = parser.getName();
139 if (tagName.equals(XML_MENU)) {
140 // Go to next tag
141 eventType = parser.next();
142 break;
143 }
144
145 throw new RuntimeException("Expecting menu, got " + tagName);
146 }
147 eventType = parser.next();
148 } while (eventType != XmlPullParser.END_DOCUMENT);
149
150 boolean reachedEndOfMenu = false;
151 while (!reachedEndOfMenu) {
152 switch (eventType) {
153 case XmlPullParser.START_TAG:
154 if (lookingForEndOfUnknownTag) {
155 break;
156 }
157
158 tagName = parser.getName();
159 if (tagName.equals(XML_GROUP)) {
160 menuState.readGroup(attrs);
161 } else if (tagName.equals(XML_ITEM)) {
162 menuState.readItem(attrs);
163 } else if (tagName.equals(XML_MENU)) {
164 // A menu start tag denotes a submenu for an item
165 SubMenu subMenu = menuState.addSubMenuItem();
166
167 // Parse the submenu into returned SubMenu
168 parseMenu(parser, attrs, subMenu);
169 } else {
170 lookingForEndOfUnknownTag = true;
171 unknownTagName = tagName;
172 }
173 break;
174
175 case XmlPullParser.END_TAG:
176 tagName = parser.getName();
177 if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
178 lookingForEndOfUnknownTag = false;
179 unknownTagName = null;
180 } else if (tagName.equals(XML_GROUP)) {
181 menuState.resetGroup();
182 } else if (tagName.equals(XML_ITEM)) {
183 // Add the item if it hasn't been added (if the item was
184 // a submenu, it would have been added already)
185 if (!menuState.hasAddedItem()) {
186 if (menuState.itemActionProvider != null &&
187 menuState.itemActionProvider.hasSubMenu()) {
188 menuState.addSubMenuItem();
189 } else {
190 menuState.addItem();
191 }
192 }
193 } else if (tagName.equals(XML_MENU)) {
194 reachedEndOfMenu = true;
195 }
196 break;
197
198 case XmlPullParser.END_DOCUMENT:
199 throw new RuntimeException("Unexpected end of document");
200 }
201
202 eventType = parser.next();
203 }
204 }
205
206 private static class InflatedOnMenuItemClickListener
207 implements MenuItem.OnMenuItemClickListener {
208 private static final Class<?>[] PARAM_TYPES = new Class[] { MenuItem.class };
209
210 private Object mRealOwner;
211 private Method mMethod;
212
213 public InflatedOnMenuItemClickListener(Object realOwner, String methodName) {
214 mRealOwner = realOwner;
215 Class<?> c = realOwner.getClass();
216 try {
217 mMethod = c.getMethod(methodName, PARAM_TYPES);
218 } catch (Exception e) {
219 InflateException ex = new InflateException(
220 "Couldn't resolve menu item onClick handler " + methodName +
221 " in class " + c.getName());
222 ex.initCause(e);
223 throw ex;
224 }
225 }
226
227 public boolean onMenuItemClick(MenuItem item) {
228 try {
229 if (mMethod.getReturnType() == Boolean.TYPE) {
230 return (Boolean) mMethod.invoke(mRealOwner, item);
231 } else {
232 mMethod.invoke(mRealOwner, item);
233 return true;
234 }
235 } catch (Exception e) {
236 throw new RuntimeException(e);
237 }
238 }
239 }
240
241 /**
242 * State for the current menu.
243 * <p>
244 * Groups can not be nested unless there is another menu (which will have
245 * its state class).
246 */
247 private class MenuState {
248 private Menu menu;
249
250 /*
251 * Group state is set on items as they are added, allowing an item to
252 * override its group state. (As opposed to set on items at the group end tag.)
253 */
254 private int groupId;
255 private int groupCategory;
256 private int groupOrder;
257 private int groupCheckable;
258 private boolean groupVisible;
259 private boolean groupEnabled;
260
261 private boolean itemAdded;
262 private int itemId;
263 private int itemCategoryOrder;
264 private CharSequence itemTitle;
265 private CharSequence itemTitleCondensed;
266 private int itemIconResId;
267 private char itemAlphabeticShortcut;
268 private char itemNumericShortcut;
269 /**
270 * Sync to attrs.xml enum:
271 * - 0: none
272 * - 1: all
273 * - 2: exclusive
274 */
275 private int itemCheckable;
276 private boolean itemChecked;
277 private boolean itemVisible;
278 private boolean itemEnabled;
279
280 /**
281 * Sync to attrs.xml enum, values in MenuItem:
282 * - 0: never
283 * - 1: ifRoom
284 * - 2: always
285 * - -1: Safe sentinel for "no value".
286 */
287 private int itemShowAsAction;
288
289 private int itemActionViewLayout;
290 private String itemActionViewClassName;
291 private String itemActionProviderClassName;
292
293 private String itemListenerMethodName;
294
295 private ActionProvider itemActionProvider;
296
297 private static final int defaultGroupId = NO_ID;
298 private static final int defaultItemId = NO_ID;
299 private static final int defaultItemCategory = 0;
300 private static final int defaultItemOrder = 0;
301 private static final int defaultItemCheckable = 0;
302 private static final boolean defaultItemChecked = false;
303 private static final boolean defaultItemVisible = true;
304 private static final boolean defaultItemEnabled = true;
305
306 public MenuState(final Menu menu) {
307 this.menu = menu;
308
309 resetGroup();
310 }
311
312 public void resetGroup() {
313 groupId = defaultGroupId;
314 groupCategory = defaultItemCategory;
315 groupOrder = defaultItemOrder;
316 groupCheckable = defaultItemCheckable;
317 groupVisible = defaultItemVisible;
318 groupEnabled = defaultItemEnabled;
319 }
320
321 /**
322 * Called when the parser is pointing to a group tag.
323 */
324 public void readGroup(AttributeSet attrs) {
325 TypedArray a = mContext.obtainStyledAttributes(attrs,
326 R.styleable.SherlockMenuGroup);
327
328 groupId = a.getResourceId(R.styleable.SherlockMenuGroup_android_id, defaultGroupId);
329 groupCategory = a.getInt(R.styleable.SherlockMenuGroup_android_menuCategory, defaultItemCategory);
330 groupOrder = a.getInt(R.styleable.SherlockMenuGroup_android_orderInCategory, defaultItemOrder);
331 groupCheckable = a.getInt(R.styleable.SherlockMenuGroup_android_checkableBehavior, defaultItemCheckable);
332 groupVisible = a.getBoolean(R.styleable.SherlockMenuGroup_android_visible, defaultItemVisible);
333 groupEnabled = a.getBoolean(R.styleable.SherlockMenuGroup_android_enabled, defaultItemEnabled);
334
335 a.recycle();
336 }
337
338 /**
339 * Called when the parser is pointing to an item tag.
340 */
341 public void readItem(AttributeSet attrs) {
342 TypedArray a = mContext.obtainStyledAttributes(attrs,
343 R.styleable.SherlockMenuItem);
344
345 // Inherit attributes from the group as default value
346 itemId = a.getResourceId(R.styleable.SherlockMenuItem_android_id, defaultItemId);
347 final int category = a.getInt(R.styleable.SherlockMenuItem_android_menuCategory, groupCategory);
348 final int order = a.getInt(R.styleable.SherlockMenuItem_android_orderInCategory, groupOrder);
349 itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK);
350 itemTitle = a.getText(R.styleable.SherlockMenuItem_android_title);
351 itemTitleCondensed = a.getText(R.styleable.SherlockMenuItem_android_titleCondensed);
352 itemIconResId = a.getResourceId(R.styleable.SherlockMenuItem_android_icon, 0);
353 itemAlphabeticShortcut =
354 getShortcut(a.getString(R.styleable.SherlockMenuItem_android_alphabeticShortcut));
355 itemNumericShortcut =
356 getShortcut(a.getString(R.styleable.SherlockMenuItem_android_numericShortcut));
357 if (a.hasValue(R.styleable.SherlockMenuItem_android_checkable)) {
358 // Item has attribute checkable, use it
359 itemCheckable = a.getBoolean(R.styleable.SherlockMenuItem_android_checkable, false) ? 1 : 0;
360 } else {
361 // Item does not have attribute, use the group's (group can have one more state
362 // for checkable that represents the exclusive checkable)
363 itemCheckable = groupCheckable;
364 }
365
366 itemChecked = a.getBoolean(R.styleable.SherlockMenuItem_android_checked, defaultItemChecked);
367 itemVisible = a.getBoolean(R.styleable.SherlockMenuItem_android_visible, groupVisible);
368 itemEnabled = a.getBoolean(R.styleable.SherlockMenuItem_android_enabled, groupEnabled);
369
370 TypedValue value = new TypedValue();
371 a.getValue(R.styleable.SherlockMenuItem_android_showAsAction, value);
372 itemShowAsAction = value.type == TypedValue.TYPE_INT_HEX ? value.data : -1;
373
374 itemListenerMethodName = a.getString(R.styleable.SherlockMenuItem_android_onClick);
375 itemActionViewLayout = a.getResourceId(R.styleable.SherlockMenuItem_android_actionLayout, 0);
376
377 // itemActionViewClassName = a.getString(R.styleable.SherlockMenuItem_android_actionViewClass);
378 value = new TypedValue();
379 a.getValue(R.styleable.SherlockMenuItem_android_actionViewClass, value);
380 itemActionViewClassName = value.type == TypedValue.TYPE_STRING ? value.string.toString() : null;
381
382 // itemActionProviderClassName = a.getString(R.styleable.SherlockMenuItem_android_actionProviderClass);
383 value = new TypedValue();
384 a.getValue(R.styleable.SherlockMenuItem_android_actionProviderClass, value);
385 itemActionProviderClassName = value.type == TypedValue.TYPE_STRING ? value.string.toString() : null;
386
387 final boolean hasActionProvider = itemActionProviderClassName != null;
388 if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) {
389 itemActionProvider = newInstance(itemActionProviderClassName,
390 ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE,
391 mActionProviderConstructorArguments);
392 } else {
393 if (hasActionProvider) {
394 Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'."
395 + " Action view already specified.");
396 }
397 itemActionProvider = null;
398 }
399
400 a.recycle();
401
402 itemAdded = false;
403 }
404
405 private char getShortcut(String shortcutString) {
406 if (shortcutString == null) {
407 return 0;
408 } else {
409 return shortcutString.charAt(0);
410 }
411 }
412
413 private void setItem(MenuItem item) {
414 item.setChecked(itemChecked)
415 .setVisible(itemVisible)
416 .setEnabled(itemEnabled)
417 .setCheckable(itemCheckable >= 1)
418 .setTitleCondensed(itemTitleCondensed)
419 .setIcon(itemIconResId)
420 .setAlphabeticShortcut(itemAlphabeticShortcut)
421 .setNumericShortcut(itemNumericShortcut);
422
423 if (itemShowAsAction >= 0) {
424 item.setShowAsAction(itemShowAsAction);
425 }
426
427 if (itemListenerMethodName != null) {
428 if (mContext.isRestricted()) {
429 throw new IllegalStateException("The android:onClick attribute cannot "
430 + "be used within a restricted context");
431 }
432 item.setOnMenuItemClickListener(
433 new InflatedOnMenuItemClickListener(mRealOwner, itemListenerMethodName));
434 }
435
436 if (itemCheckable >= 2) {
437 if (item instanceof MenuItemImpl) {
438 MenuItemImpl impl = (MenuItemImpl) item;
439 impl.setExclusiveCheckable(true);
440 } else {
441 menu.setGroupCheckable(groupId, true, true);
442 }
443 }
444
445 boolean actionViewSpecified = false;
446 if (itemActionViewClassName != null) {
447 View actionView = (View) newInstance(itemActionViewClassName,
448 ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments);
449 item.setActionView(actionView);
450 actionViewSpecified = true;
451 }
452 if (itemActionViewLayout > 0) {
453 if (!actionViewSpecified) {
454 item.setActionView(itemActionViewLayout);
455 actionViewSpecified = true;
456 } else {
457 Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'."
458 + " Action view already specified.");
459 }
460 }
461 if (itemActionProvider != null) {
462 item.setActionProvider(itemActionProvider);
463 }
464 }
465
466 public void addItem() {
467 itemAdded = true;
468 setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle));
469 }
470
471 public SubMenu addSubMenuItem() {
472 itemAdded = true;
473 SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);
474 setItem(subMenu.getItem());
475 return subMenu;
476 }
477
478 public boolean hasAddedItem() {
479 return itemAdded;
480 }
481
482 @SuppressWarnings("unchecked")
483 private <T> T newInstance(String className, Class<?>[] constructorSignature,
484 Object[] arguments) {
485 try {
486 Class<?> clazz = mContext.getClassLoader().loadClass(className);
487 Constructor<?> constructor = clazz.getConstructor(constructorSignature);
488 return (T) constructor.newInstance(arguments);
489 } catch (Exception e) {
490 Log.w(LOG_TAG, "Cannot instantiate class: " + className, e);
491 }
492 return null;
493 }
494 }
495 }