/* * Copyright (C) 2006 The Android Open Source Project * 2011 Jake Wharton * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v4.view; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.content.Context; import android.content.res.XmlResourceParser; import android.util.AttributeSet; import android.util.Xml; import android.view.InflateException; import android.view.View; import com.actionbarsherlock.internal.view.menu.MenuBuilder; import com.actionbarsherlock.internal.view.menu.MenuItemImpl; import com.actionbarsherlock.internal.view.menu.SubMenuBuilder; /** * This class is used to instantiate menu XML files into Menu objects. * <p> * For performance reasons, menu inflation relies heavily on pre-processing of * XML files that is done at build time. Therefore, it is not currently possible * to use MenuInflater with an XmlPullParser over a plain XML file at runtime; * it only works with an XmlPullParser returned from a compiled resource (R. * <em>something</em> file.) */ public final class MenuInflater extends android.view.MenuInflater { private static final Class<?>[] ACTION_VIEW_CONSTRUCTOR_SIGNATURE = new Class[] { Context.class }; private static final Class<?>[] PARAM_TYPES = new Class[] { android.support.v4.view.MenuItem.class }; /** Android XML namespace. */ private static final String XML_NS = "http://schemas.android.com/apk/res/android"; /** Menu tag name in XML. */ private static final String XML_MENU = "menu"; /** Group tag name in XML. */ private static final String XML_GROUP = "group"; /** Item tag name in XML. */ private static final String XML_ITEM = "item"; /** Context from which to inflate resources. */ private final Context mContext; /** Native inflater for context menu fallback. */ private final android.view.MenuInflater mNativeMenuInflater; /** * Constructs a menu inflater. * * @see Activity#getMenuInflater() */ public MenuInflater(Context context, android.view.MenuInflater nativeMenuInflater) { super(context); mContext = context; mNativeMenuInflater = nativeMenuInflater; } /** * Inflate a menu hierarchy from the specified XML resource. Throws * {@link InflateException} if there is an error. * * @param menuRes Resource ID for an XML layout resource to load (e.g., * <code>R.menu.main_activity</code>) * @param menu The Menu to inflate into. The items and submenus will be * added to this Menu. */ @Override public void inflate(int menuRes, android.view.Menu menu) { if (!(menu instanceof MenuBuilder)) { mNativeMenuInflater.inflate(menuRes, menu); return; } MenuBuilder actionBarMenu = (MenuBuilder)menu; XmlResourceParser parser = null; try { parser = mContext.getResources().getLayout(menuRes); AttributeSet attrs = Xml.asAttributeSet(parser); parseMenu(parser, attrs, actionBarMenu); } catch (XmlPullParserException e) { throw new InflateException("Error inflating menu XML", e); } catch (IOException e) { throw new InflateException("Error inflating menu XML", e); } finally { if (parser != null) parser.close(); } } /** * Called internally to fill the given menu. If a sub menu is seen, it will * call this recursively. */ private void parseMenu(XmlPullParser parser, AttributeSet attrs, MenuBuilder menu) throws XmlPullParserException, IOException { ActionBarMenuState menuState = new ActionBarMenuState(menu); int eventType = parser.getEventType(); String tagName; boolean lookingForEndOfUnknownTag = false; String unknownTagName = null; // This loop will skip to the menu start tag do { if (eventType == XmlPullParser.START_TAG) { tagName = parser.getName(); if (tagName.equals(XML_MENU)) { // Go to next tag eventType = parser.next(); break; } throw new RuntimeException("Expecting menu, got " + tagName); } eventType = parser.next(); } while (eventType != XmlPullParser.END_DOCUMENT); boolean reachedEndOfMenu = false; while (!reachedEndOfMenu) { switch (eventType) { case XmlPullParser.START_TAG: if (lookingForEndOfUnknownTag) { break; } tagName = parser.getName(); if (tagName.equals(XML_GROUP)) { menuState.readGroup(attrs); } else if (tagName.equals(XML_ITEM)) { menuState.readItem(attrs); } else if (tagName.equals(XML_MENU)) { // A menu start tag denotes a submenu for an item SubMenuBuilder subMenu = menuState.addSubMenuItem(); // Parse the submenu into returned SubMenu parseMenu(parser, attrs, subMenu); } else { lookingForEndOfUnknownTag = true; unknownTagName = tagName; } break; case XmlPullParser.END_TAG: tagName = parser.getName(); if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) { lookingForEndOfUnknownTag = false; unknownTagName = null; } else if (tagName.equals(XML_GROUP)) { menuState.resetGroup(); } else if (tagName.equals(XML_ITEM)) { // Add the item if it hasn't been added (if the item was // a submenu, it would have been added already) if (!menuState.hasAddedItem()) { menuState.addItem(); } } else if (tagName.equals(XML_MENU)) { reachedEndOfMenu = true; } break; case XmlPullParser.END_DOCUMENT: throw new RuntimeException("Unexpected end of document"); } eventType = parser.next(); } } /** * State for the current menu. * <p> * Groups can not be nested unless there is another menu (which will have * its state class). */ private final class ActionBarMenuState { private final MenuBuilder menu; /* * Group state is set on items as they are added, allowing an item to * override its group state. (As opposed to set on items at the group end tag.) */ private int groupId; private int groupCategory; private int groupOrder; private int groupCheckable; private boolean groupVisible; private boolean groupEnabled; private boolean itemAdded; private int itemId; private int itemCategoryOrder; private String itemTitle; private String itemTitleCondensed; private int itemIconResId; private char itemAlphabeticShortcut; private char itemNumericShortcut; /** * Sync to attrs.xml enum: * - 0: none * - 1: all * - 2: exclusive */ private int itemCheckable; private boolean itemChecked; private boolean itemVisible; private boolean itemEnabled; private String itemListenerMethodName; private int itemShowAsAction; private int itemActionLayout; private String itemActionViewClassName; private static final int defaultGroupId = View.NO_ID; private static final int defaultItemId = View.NO_ID; private static final int defaultItemCategory = 0; private static final int defaultItemOrder = 0; private static final int defaultItemCheckable = 0; private static final boolean defaultItemChecked = false; private static final boolean defaultItemVisible = true; private static final boolean defaultItemEnabled = true; private static final int defaultItemShowAsAction = 0; private static final int defaultIconResId = View.NO_ID; /** Mirror of package-scoped Menu.CATEGORY_MASK. */ private static final int Menu__CATEGORY_MASK = 0xffff0000; /** Mirror of package-scoped Menu.USER_MASK. */ private static final int Menu__USER_MASK = 0x0000ffff; public ActionBarMenuState(MenuBuilder menu) { this.menu = menu; resetGroup(); } public void resetGroup() { groupId = defaultGroupId; groupCategory = defaultItemCategory; groupOrder = defaultItemOrder; groupCheckable = defaultItemCheckable; groupVisible = defaultItemVisible; groupEnabled = defaultItemEnabled; } /** * Called when the parser is pointing to a group tag. */ public void readGroup(AttributeSet attrs) { //TypedArray a = mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.MenuGroup); //groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId); groupId = attrs.getAttributeResourceValue(XML_NS, "id", defaultGroupId); //groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory); groupCategory = attrs.getAttributeIntValue(XML_NS, "menuCategory", defaultItemCategory); //groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder); groupOrder = attrs.getAttributeIntValue(XML_NS, "orderInCategory", defaultItemOrder); //groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable); groupCheckable = attrs.getAttributeIntValue(XML_NS, "checkableBehavior", defaultItemCheckable); //groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible); groupVisible = attrs.getAttributeBooleanValue(XML_NS, "visible", defaultItemVisible); //groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled); groupEnabled = attrs.getAttributeBooleanValue(XML_NS, "enabled", defaultItemEnabled); //a.recycle(); } /** * Called when the parser is pointing to an item tag. */ public void readItem(AttributeSet attrs) { //TypedArray a = mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.MenuItem); // Inherit attributes from the group as default value //itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId); itemId = attrs.getAttributeResourceValue(XML_NS, "id", defaultItemId); //final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory); final int category = attrs.getAttributeIntValue(XML_NS, "menuCategory", groupCategory); //final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder); final int order = attrs.getAttributeIntValue(XML_NS, "orderInCategory", groupOrder); //itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK); itemCategoryOrder = (category & Menu__CATEGORY_MASK) | (order & Menu__USER_MASK); //itemTitle = a.getString(com.android.internal.R.styleable.MenuItem_title); final int itemTitleId = attrs.getAttributeResourceValue(XML_NS, "title", 0); if (itemTitleId != 0) { itemTitle = mContext.getString(itemTitleId); } else { itemTitle = attrs.getAttributeValue(XML_NS, "title"); } //itemTitleCondensed = a.getString(com.android.internal.R.styleable.MenuItem_titleCondensed); final int itemTitleCondensedId = attrs.getAttributeResourceValue(XML_NS, "titleCondensed", 0); if (itemTitleCondensedId != 0) { itemTitleCondensed = mContext.getString(itemTitleCondensedId); } else { itemTitleCondensed = attrs.getAttributeValue(XML_NS, "titleCondensed"); } //itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0); itemIconResId = attrs.getAttributeResourceValue(XML_NS, "icon", defaultIconResId); //itemAlphabeticShortcut = getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut)); itemAlphabeticShortcut = getShortcut(attrs.getAttributeValue(XML_NS, "alphabeticShortcut")); //itemNumericShortcut = getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut)); itemNumericShortcut = getShortcut(attrs.getAttributeValue(XML_NS, "numericShortcut")); //if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) { if (attrs.getAttributeValue(XML_NS, "checkable") != null) { // Item has attribute checkable, use it //itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0; itemCheckable = attrs.getAttributeBooleanValue(XML_NS, "checkable", false) ? 1 : 0; } else { // Item does not have attribute, use the group's (group can have one more state // for checkable that represents the exclusive checkable) itemCheckable = groupCheckable; } //itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked); itemChecked = attrs.getAttributeBooleanValue(XML_NS, "checked", defaultItemChecked); //itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible); itemVisible = attrs.getAttributeBooleanValue(XML_NS, "visible", groupVisible); //itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled); itemEnabled = attrs.getAttributeBooleanValue(XML_NS, "enabled", groupEnabled); //presumed emulation of 3.0+'s MenuInflator: itemListenerMethodName = attrs.getAttributeValue(XML_NS, "onClick"); itemShowAsAction = attrs.getAttributeIntValue(XML_NS, "showAsAction", defaultItemShowAsAction); itemActionLayout = attrs.getAttributeResourceValue(XML_NS, "actionLayout", 0); itemActionViewClassName = attrs.getAttributeValue(XML_NS, "actionViewClass"); //a.recycle(); itemAdded = false; } private char getShortcut(String shortcutString) { if (shortcutString == null) { return 0; } else { return shortcutString.charAt(0); } } private void setItem(MenuItemImpl item) { item.setChecked(itemChecked) .setVisible(itemVisible) .setEnabled(itemEnabled) .setCheckable(itemCheckable >= 1) .setTitleCondensed(itemTitleCondensed) .setIcon(itemIconResId) .setAlphabeticShortcut(itemAlphabeticShortcut) .setNumericShortcut(itemNumericShortcut); if (itemShowAsAction > 0) { item.setShowAsAction(itemShowAsAction); } if (itemListenerMethodName != null) { if (MenuInflater.this.mContext.isRestricted()) { throw new IllegalStateException("The android:onClick attribute cannot be used within a restricted context"); } item.setOnMenuItemClickListener(new InflatedOnMenuItemClickListener(itemListenerMethodName)); } if (itemCheckable >= 2) { item.setExclusiveCheckable(true); } if (itemActionViewClassName != null) { try { Context context = MenuInflater.this.mContext; ClassLoader loader = context.getClassLoader(); Class<?> actionViewClass = Class.forName(itemActionViewClassName, true, loader); Constructor<?> constructor = actionViewClass.getConstructor(ACTION_VIEW_CONSTRUCTOR_SIGNATURE); View actionView = (View)constructor.newInstance(new Object[] { context }); item.setActionView(actionView); } catch (Exception e) { throw new InflateException(e); } } else if (itemActionLayout > 0) { item.setActionView(itemActionLayout); } } public void addItem() { itemAdded = true; setItem((MenuItemImpl)menu.add(groupId, itemId, itemCategoryOrder, itemTitle)); } public SubMenuBuilder addSubMenuItem() { itemAdded = true; SubMenuBuilder subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle); setItem(subMenu.getItem()); return subMenu; } public boolean hasAddedItem() { return itemAdded; } } class InflatedOnMenuItemClickListener extends android.support.v4.view.MenuItem.OnMenuItemClickListener { private Method mMethod; public InflatedOnMenuItemClickListener(String methodName) { final Class<?> localClass = mContext.getClass(); try { mMethod = localClass.getMethod(methodName, PARAM_TYPES); } catch (Exception e) { StringBuilder b = new StringBuilder(); b.append("Couldn't resolve menu item onClick handler "); b.append(methodName); b.append(" in class "); b.append(localClass.getName()); throw new InflateException(b.toString(), e); } } @Override public boolean onMenuItemClick(MenuItem item) { final Object[] params = new Object[] { item }; try { if (mMethod.getReturnType() == Boolean.TYPE) { return (Boolean)mMethod.invoke(mContext, params); } return false; } catch (Exception e) { throw new RuntimeException(e); } } } }