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