--- /dev/null
+/*\r
+ * Copyright (C) 2006 The Android Open Source Project\r
+ * 2011 Jake Wharton\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package com.actionbarsherlock.view;\r
+\r
+import java.io.IOException;\r
+import java.lang.reflect.Constructor;\r
+import java.lang.reflect.Method;\r
+import org.xmlpull.v1.XmlPullParser;\r
+import org.xmlpull.v1.XmlPullParserException;\r
+import android.content.Context;\r
+import android.content.res.TypedArray;\r
+import android.content.res.XmlResourceParser;\r
+import android.util.AttributeSet;\r
+import android.util.Log;\r
+import android.util.TypedValue;\r
+import android.util.Xml;\r
+import android.view.InflateException;\r
+import android.view.View;\r
+\r
+import com.actionbarsherlock.R;\r
+import com.actionbarsherlock.internal.view.menu.MenuItemImpl;\r
+\r
+/**\r
+ * This class is used to instantiate menu XML files into Menu objects.\r
+ * <p>\r
+ * For performance reasons, menu inflation relies heavily on pre-processing of\r
+ * XML files that is done at build time. Therefore, it is not currently possible\r
+ * to use MenuInflater with an XmlPullParser over a plain XML file at runtime;\r
+ * it only works with an XmlPullParser returned from a compiled resource (R.\r
+ * <em>something</em> file.)\r
+ */\r
+public class MenuInflater {\r
+ private static final String LOG_TAG = "MenuInflater";\r
+\r
+ /** Menu tag name in XML. */\r
+ private static final String XML_MENU = "menu";\r
+\r
+ /** Group tag name in XML. */\r
+ private static final String XML_GROUP = "group";\r
+\r
+ /** Item tag name in XML. */\r
+ private static final String XML_ITEM = "item";\r
+\r
+ private static final int NO_ID = 0;\r
+\r
+ private static final Class<?>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] {Context.class};\r
+\r
+ private static final Class<?>[] ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE = ACTION_VIEW_CONSTRUCTOR_SIGNATURE;\r
+\r
+ private final Object[] mActionViewConstructorArguments;\r
+\r
+ private final Object[] mActionProviderConstructorArguments;\r
+\r
+ private Context mContext;\r
+\r
+ /**\r
+ * Constructs a menu inflater.\r
+ *\r
+ * @see Activity#getMenuInflater()\r
+ */\r
+ public MenuInflater(Context context) {\r
+ mContext = context;\r
+ mActionViewConstructorArguments = new Object[] {context};\r
+ mActionProviderConstructorArguments = mActionViewConstructorArguments;\r
+ }\r
+\r
+ /**\r
+ * Inflate a menu hierarchy from the specified XML resource. Throws\r
+ * {@link InflateException} if there is an error.\r
+ *\r
+ * @param menuRes Resource ID for an XML layout resource to load (e.g.,\r
+ * <code>R.menu.main_activity</code>)\r
+ * @param menu The Menu to inflate into. The items and submenus will be\r
+ * added to this Menu.\r
+ */\r
+ public void inflate(int menuRes, Menu menu) {\r
+ XmlResourceParser parser = null;\r
+ try {\r
+ parser = mContext.getResources().getLayout(menuRes);\r
+ AttributeSet attrs = Xml.asAttributeSet(parser);\r
+\r
+ parseMenu(parser, attrs, menu);\r
+ } catch (XmlPullParserException e) {\r
+ throw new InflateException("Error inflating menu XML", e);\r
+ } catch (IOException e) {\r
+ throw new InflateException("Error inflating menu XML", e);\r
+ } finally {\r
+ if (parser != null) parser.close();\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Called internally to fill the given menu. If a sub menu is seen, it will\r
+ * call this recursively.\r
+ */\r
+ private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)\r
+ throws XmlPullParserException, IOException {\r
+ MenuState menuState = new MenuState(menu);\r
+\r
+ int eventType = parser.getEventType();\r
+ String tagName;\r
+ boolean lookingForEndOfUnknownTag = false;\r
+ String unknownTagName = null;\r
+\r
+ // This loop will skip to the menu start tag\r
+ do {\r
+ if (eventType == XmlPullParser.START_TAG) {\r
+ tagName = parser.getName();\r
+ if (tagName.equals(XML_MENU)) {\r
+ // Go to next tag\r
+ eventType = parser.next();\r
+ break;\r
+ }\r
+\r
+ throw new RuntimeException("Expecting menu, got " + tagName);\r
+ }\r
+ eventType = parser.next();\r
+ } while (eventType != XmlPullParser.END_DOCUMENT);\r
+\r
+ boolean reachedEndOfMenu = false;\r
+ while (!reachedEndOfMenu) {\r
+ switch (eventType) {\r
+ case XmlPullParser.START_TAG:\r
+ if (lookingForEndOfUnknownTag) {\r
+ break;\r
+ }\r
+\r
+ tagName = parser.getName();\r
+ if (tagName.equals(XML_GROUP)) {\r
+ menuState.readGroup(attrs);\r
+ } else if (tagName.equals(XML_ITEM)) {\r
+ menuState.readItem(attrs);\r
+ } else if (tagName.equals(XML_MENU)) {\r
+ // A menu start tag denotes a submenu for an item\r
+ SubMenu subMenu = menuState.addSubMenuItem();\r
+\r
+ // Parse the submenu into returned SubMenu\r
+ parseMenu(parser, attrs, subMenu);\r
+ } else {\r
+ lookingForEndOfUnknownTag = true;\r
+ unknownTagName = tagName;\r
+ }\r
+ break;\r
+\r
+ case XmlPullParser.END_TAG:\r
+ tagName = parser.getName();\r
+ if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {\r
+ lookingForEndOfUnknownTag = false;\r
+ unknownTagName = null;\r
+ } else if (tagName.equals(XML_GROUP)) {\r
+ menuState.resetGroup();\r
+ } else if (tagName.equals(XML_ITEM)) {\r
+ // Add the item if it hasn't been added (if the item was\r
+ // a submenu, it would have been added already)\r
+ if (!menuState.hasAddedItem()) {\r
+ if (menuState.itemActionProvider != null &&\r
+ menuState.itemActionProvider.hasSubMenu()) {\r
+ menuState.addSubMenuItem();\r
+ } else {\r
+ menuState.addItem();\r
+ }\r
+ }\r
+ } else if (tagName.equals(XML_MENU)) {\r
+ reachedEndOfMenu = true;\r
+ }\r
+ break;\r
+\r
+ case XmlPullParser.END_DOCUMENT:\r
+ throw new RuntimeException("Unexpected end of document");\r
+ }\r
+\r
+ eventType = parser.next();\r
+ }\r
+ }\r
+\r
+ private static class InflatedOnMenuItemClickListener\r
+ implements MenuItem.OnMenuItemClickListener {\r
+ private static final Class<?>[] PARAM_TYPES = new Class[] { MenuItem.class };\r
+\r
+ private Context mContext;\r
+ private Method mMethod;\r
+\r
+ public InflatedOnMenuItemClickListener(Context context, String methodName) {\r
+ mContext = context;\r
+ Class<?> c = context.getClass();\r
+ try {\r
+ mMethod = c.getMethod(methodName, PARAM_TYPES);\r
+ } catch (Exception e) {\r
+ InflateException ex = new InflateException(\r
+ "Couldn't resolve menu item onClick handler " + methodName +\r
+ " in class " + c.getName());\r
+ ex.initCause(e);\r
+ throw ex;\r
+ }\r
+ }\r
+\r
+ public boolean onMenuItemClick(MenuItem item) {\r
+ try {\r
+ if (mMethod.getReturnType() == Boolean.TYPE) {\r
+ return (Boolean) mMethod.invoke(mContext, item);\r
+ } else {\r
+ mMethod.invoke(mContext, item);\r
+ return true;\r
+ }\r
+ } catch (Exception e) {\r
+ throw new RuntimeException(e);\r
+ }\r
+ }\r
+ }\r
+\r
+ /**\r
+ * State for the current menu.\r
+ * <p>\r
+ * Groups can not be nested unless there is another menu (which will have\r
+ * its state class).\r
+ */\r
+ private class MenuState {\r
+ private Menu menu;\r
+\r
+ /*\r
+ * Group state is set on items as they are added, allowing an item to\r
+ * override its group state. (As opposed to set on items at the group end tag.)\r
+ */\r
+ private int groupId;\r
+ private int groupCategory;\r
+ private int groupOrder;\r
+ private int groupCheckable;\r
+ private boolean groupVisible;\r
+ private boolean groupEnabled;\r
+\r
+ private boolean itemAdded;\r
+ private int itemId;\r
+ private int itemCategoryOrder;\r
+ private CharSequence itemTitle;\r
+ private CharSequence itemTitleCondensed;\r
+ private int itemIconResId;\r
+ private char itemAlphabeticShortcut;\r
+ private char itemNumericShortcut;\r
+ /**\r
+ * Sync to attrs.xml enum:\r
+ * - 0: none\r
+ * - 1: all\r
+ * - 2: exclusive\r
+ */\r
+ private int itemCheckable;\r
+ private boolean itemChecked;\r
+ private boolean itemVisible;\r
+ private boolean itemEnabled;\r
+\r
+ /**\r
+ * Sync to attrs.xml enum, values in MenuItem:\r
+ * - 0: never\r
+ * - 1: ifRoom\r
+ * - 2: always\r
+ * - -1: Safe sentinel for "no value".\r
+ */\r
+ private int itemShowAsAction;\r
+\r
+ private int itemActionViewLayout;\r
+ private String itemActionViewClassName;\r
+ private String itemActionProviderClassName;\r
+\r
+ private String itemListenerMethodName;\r
+\r
+ private ActionProvider itemActionProvider;\r
+\r
+ private static final int defaultGroupId = NO_ID;\r
+ private static final int defaultItemId = NO_ID;\r
+ private static final int defaultItemCategory = 0;\r
+ private static final int defaultItemOrder = 0;\r
+ private static final int defaultItemCheckable = 0;\r
+ private static final boolean defaultItemChecked = false;\r
+ private static final boolean defaultItemVisible = true;\r
+ private static final boolean defaultItemEnabled = true;\r
+\r
+ public MenuState(final Menu menu) {\r
+ this.menu = menu;\r
+\r
+ resetGroup();\r
+ }\r
+\r
+ public void resetGroup() {\r
+ groupId = defaultGroupId;\r
+ groupCategory = defaultItemCategory;\r
+ groupOrder = defaultItemOrder;\r
+ groupCheckable = defaultItemCheckable;\r
+ groupVisible = defaultItemVisible;\r
+ groupEnabled = defaultItemEnabled;\r
+ }\r
+\r
+ /**\r
+ * Called when the parser is pointing to a group tag.\r
+ */\r
+ public void readGroup(AttributeSet attrs) {\r
+ TypedArray a = mContext.obtainStyledAttributes(attrs,\r
+ R.styleable.SherlockMenuGroup);\r
+\r
+ groupId = a.getResourceId(R.styleable.SherlockMenuGroup_android_id, defaultGroupId);\r
+ groupCategory = a.getInt(R.styleable.SherlockMenuGroup_android_menuCategory, defaultItemCategory);\r
+ groupOrder = a.getInt(R.styleable.SherlockMenuGroup_android_orderInCategory, defaultItemOrder);\r
+ groupCheckable = a.getInt(R.styleable.SherlockMenuGroup_android_checkableBehavior, defaultItemCheckable);\r
+ groupVisible = a.getBoolean(R.styleable.SherlockMenuGroup_android_visible, defaultItemVisible);\r
+ groupEnabled = a.getBoolean(R.styleable.SherlockMenuGroup_android_enabled, defaultItemEnabled);\r
+\r
+ a.recycle();\r
+ }\r
+\r
+ /**\r
+ * Called when the parser is pointing to an item tag.\r
+ */\r
+ public void readItem(AttributeSet attrs) {\r
+ TypedArray a = mContext.obtainStyledAttributes(attrs,\r
+ R.styleable.SherlockMenuItem);\r
+\r
+ // Inherit attributes from the group as default value\r
+ itemId = a.getResourceId(R.styleable.SherlockMenuItem_android_id, defaultItemId);\r
+ final int category = a.getInt(R.styleable.SherlockMenuItem_android_menuCategory, groupCategory);\r
+ final int order = a.getInt(R.styleable.SherlockMenuItem_android_orderInCategory, groupOrder);\r
+ itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK);\r
+ itemTitle = a.getText(R.styleable.SherlockMenuItem_android_title);\r
+ itemTitleCondensed = a.getText(R.styleable.SherlockMenuItem_android_titleCondensed);\r
+ itemIconResId = a.getResourceId(R.styleable.SherlockMenuItem_android_icon, 0);\r
+ itemAlphabeticShortcut =\r
+ getShortcut(a.getString(R.styleable.SherlockMenuItem_android_alphabeticShortcut));\r
+ itemNumericShortcut =\r
+ getShortcut(a.getString(R.styleable.SherlockMenuItem_android_numericShortcut));\r
+ if (a.hasValue(R.styleable.SherlockMenuItem_android_checkable)) {\r
+ // Item has attribute checkable, use it\r
+ itemCheckable = a.getBoolean(R.styleable.SherlockMenuItem_android_checkable, false) ? 1 : 0;\r
+ } else {\r
+ // Item does not have attribute, use the group's (group can have one more state\r
+ // for checkable that represents the exclusive checkable)\r
+ itemCheckable = groupCheckable;\r
+ }\r
+\r
+ itemChecked = a.getBoolean(R.styleable.SherlockMenuItem_android_checked, defaultItemChecked);\r
+ itemVisible = a.getBoolean(R.styleable.SherlockMenuItem_android_visible, groupVisible);\r
+ itemEnabled = a.getBoolean(R.styleable.SherlockMenuItem_android_enabled, groupEnabled);\r
+\r
+ TypedValue value = new TypedValue();\r
+ a.getValue(R.styleable.SherlockMenuItem_android_showAsAction, value);\r
+ itemShowAsAction = value.type == TypedValue.TYPE_INT_HEX ? value.data : -1;\r
+\r
+ itemListenerMethodName = a.getString(R.styleable.SherlockMenuItem_android_onClick);\r
+ itemActionViewLayout = a.getResourceId(R.styleable.SherlockMenuItem_android_actionLayout, 0);\r
+ itemActionViewClassName = a.getString(R.styleable.SherlockMenuItem_android_actionViewClass);\r
+ itemActionProviderClassName = a.getString(R.styleable.SherlockMenuItem_android_actionProviderClass);\r
+\r
+ final boolean hasActionProvider = itemActionProviderClassName != null;\r
+ if (hasActionProvider && itemActionViewLayout == 0 && itemActionViewClassName == null) {\r
+ itemActionProvider = newInstance(itemActionProviderClassName,\r
+ ACTION_PROVIDER_CONSTRUCTOR_SIGNATURE,\r
+ mActionProviderConstructorArguments);\r
+ } else {\r
+ if (hasActionProvider) {\r
+ Log.w(LOG_TAG, "Ignoring attribute 'actionProviderClass'."\r
+ + " Action view already specified.");\r
+ }\r
+ itemActionProvider = null;\r
+ }\r
+\r
+ a.recycle();\r
+\r
+ itemAdded = false;\r
+ }\r
+\r
+ private char getShortcut(String shortcutString) {\r
+ if (shortcutString == null) {\r
+ return 0;\r
+ } else {\r
+ return shortcutString.charAt(0);\r
+ }\r
+ }\r
+\r
+ private void setItem(MenuItem item) {\r
+ item.setChecked(itemChecked)\r
+ .setVisible(itemVisible)\r
+ .setEnabled(itemEnabled)\r
+ .setCheckable(itemCheckable >= 1)\r
+ .setTitleCondensed(itemTitleCondensed)\r
+ .setIcon(itemIconResId)\r
+ .setAlphabeticShortcut(itemAlphabeticShortcut)\r
+ .setNumericShortcut(itemNumericShortcut);\r
+\r
+ if (itemShowAsAction >= 0) {\r
+ item.setShowAsAction(itemShowAsAction);\r
+ }\r
+\r
+ if (itemListenerMethodName != null) {\r
+ if (mContext.isRestricted()) {\r
+ throw new IllegalStateException("The android:onClick attribute cannot "\r
+ + "be used within a restricted context");\r
+ }\r
+ item.setOnMenuItemClickListener(\r
+ new InflatedOnMenuItemClickListener(mContext, itemListenerMethodName));\r
+ }\r
+\r
+ if (itemCheckable >= 2) {\r
+ if (item instanceof MenuItemImpl) {\r
+ MenuItemImpl impl = (MenuItemImpl) item;\r
+ impl.setExclusiveCheckable(true);\r
+ } else {\r
+ menu.setGroupCheckable(groupId, true, true);\r
+ }\r
+ }\r
+\r
+ boolean actionViewSpecified = false;\r
+ if (itemActionViewClassName != null) {\r
+ View actionView = (View) newInstance(itemActionViewClassName,\r
+ ACTION_VIEW_CONSTRUCTOR_SIGNATURE, mActionViewConstructorArguments);\r
+ item.setActionView(actionView);\r
+ actionViewSpecified = true;\r
+ }\r
+ if (itemActionViewLayout > 0) {\r
+ if (!actionViewSpecified) {\r
+ item.setActionView(itemActionViewLayout);\r
+ actionViewSpecified = true;\r
+ } else {\r
+ Log.w(LOG_TAG, "Ignoring attribute 'itemActionViewLayout'."\r
+ + " Action view already specified.");\r
+ }\r
+ }\r
+ if (itemActionProvider != null) {\r
+ item.setActionProvider(itemActionProvider);\r
+ }\r
+ }\r
+\r
+ public void addItem() {\r
+ itemAdded = true;\r
+ setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle));\r
+ }\r
+\r
+ public SubMenu addSubMenuItem() {\r
+ itemAdded = true;\r
+ SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);\r
+ setItem(subMenu.getItem());\r
+ return subMenu;\r
+ }\r
+\r
+ public boolean hasAddedItem() {\r
+ return itemAdded;\r
+ }\r
+\r
+ @SuppressWarnings("unchecked")\r
+ private <T> T newInstance(String className, Class<?>[] constructorSignature,\r
+ Object[] arguments) {\r
+ try {\r
+ Class<?> clazz = mContext.getClassLoader().loadClass(className);\r
+ Constructor<?> constructor = clazz.getConstructor(constructorSignature);\r
+ return (T) constructor.newInstance(arguments);\r
+ } catch (Exception e) {\r
+ Log.w(LOG_TAG, "Cannot instantiate class: " + className, e);\r
+ }\r
+ return null;\r
+ }\r
+ }\r
+}\r