blob: 64b300cb9347a94dbfbafad2c532096a5be639ff [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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 com.android.tools.idea.rendering;
import com.android.ide.common.rendering.api.ILayoutPullParser;
import com.android.ide.common.resources.ResourceResolver;
import com.android.ide.common.xml.XmlPrettyPrinter;
import com.android.resources.ResourceType;
import com.android.sdklib.IAndroidTarget;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.model.ManifestInfo;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.android.SdkConstants.*;
/**
* Renderer which creates a preview of menus and renders them into a layout XML element hierarchy
* <p>
* See
* http://developer.android.com/guide/topics/ui/menus.html
* http://developer.android.com/guide/topics/resources/menu-resource.html
*
* <p>
* TODO:
* <ul>
* <li> Should we handle actionLayout and actionViewClass attributes on menu items?</li>
* <li> Handle action bar compat and action bar sherlock resources for actionbar preview?</li>
* <li> Handle Gingerbread style menus (and earlier) ?</li>
* <li> Be more resilient for custom themes not inheriting the necessary menu resources</li>
* </ul>
*/
public class MenuPreviewRenderer extends LayoutPullParserFactory {
private static final String ATTR_ORDER_IN_CATEGORY = "orderInCategory";
private static final String ATTR_MENU_CATEGORY = "menuCategory";
private static final String ATTR_CHECKABLE = "checkable";
private static final String ATTR_ALPHABETIC_SHORTCUT = "alphabeticShortcut";
private static final String ATTR_DUPLICATE_PARENT_STATE = "duplicateParentState";
private static final String ATTR_NUMERIC_SHORTCUT = "numericShortcut";
private static final String ATTR_CHECKABLE_BEHAVIOR = "checkableBehavior";
private static final String ATTR_FOCUSABLE = "focusable";
private static final String ATTR_CLICKABLE = "clickable";
private static final String ATTR_ELLIPSIZE = "ellipsize";
private static final String ATTR_FADING_EDGE = "fadingEdge";
public static final String ATTR_TEXT_COLOR = "textColor";
public static final String ATTR_TEXT_ALIGNMENT = "textAlignment";
public static final String ATTR_TEXT_APPEARANCE = "textAppearance";
private static final String VALUE_MARQUEE = "marquee";
private static final String VALUE_SINGLE = "single";
private static final String VALUE_ALL = "all";
private static final String VALUE_WITH_TEXT = "withText";
private static final String VALUE_NEVER = "never";
private final ResourceResolver myResolver;
private final Document myDocument;
private final Module myModule;
private final int myApiLevel;
private final Map<Element, Object> viewCookies = Maps.newHashMap();
private boolean myThemeIsLight;
private final XmlTag myRootTag;
public MenuPreviewRenderer(RenderTask renderTask, XmlFile file) {
myRootTag = file.getRootTag();
myResolver = renderTask.getResourceResolver();
assert myResolver != null;
myDocument = DomPullParser.createEmptyPlainDocument();
assert myDocument != null;
myModule = renderTask.getModule();
Configuration configuration = renderTask.getConfiguration();
IAndroidTarget target = configuration.getTarget();
myApiLevel = target != null ? target.getVersion().getApiLevel() : 1;
myThemeIsLight = isLightTheme(myResolver);
}
private static boolean isLightTheme(ResourceResolver resolver) {
String current = (resolver.isProjectTheme() ? STYLE_RESOURCE_PREFIX : ANDROID_STYLE_RESOURCE_PREFIX) + resolver.getThemeName();
return resolver.themeExtends("@android:style/Theme.Light", current);
}
public ILayoutPullParser render() {
if (myRootTag == null) {
return createEmptyParser();
}
// Build up a menu layout based on what we find in the menu file
// This is *simulating* what happens in an Android app. We should get first class
// menu rendering support in layoutlib to properly handle this.
Element root = addRootElement(myDocument, LINEAR_LAYOUT);
setAndroidAttr(root, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT);
setAndroidAttr(root, ATTR_LAYOUT_HEIGHT, VALUE_FILL_PARENT);
setAndroidAttr(root, ATTR_ORIENTATION, VALUE_VERTICAL);
// Create representative action bar?
boolean createdActionBar = false;
if (myApiLevel >= 11) {
createdActionBar = addActionBar(root);
}
// Next create menu
Element popup = createMenuPopup();
if (createdActionBar) {
setAndroidAttr(popup, ATTR_LAYOUT_MARGIN_TOP, "-10dp");
}
root.appendChild(popup);
populateMenu(popup);
addFidelityWarning(myDocument, root, "Menu");
if (DEBUG) {
//noinspection UseOfSystemOutOrSystemErr
System.out.println(XmlPrettyPrinter.prettyPrint(myDocument, true));
}
return new DomPullParser(myDocument.getDocumentElement()).setViewCookies(viewCookies);
}
private Element createActionBar() {
Element layout = myDocument.createElement(LINEAR_LAYOUT);
setAndroidAttr(layout, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT);
setAndroidAttr(layout, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
setAndroidAttr(layout, ATTR_LAYOUT_GRAVITY, GRAVITY_VALUE_RIGHT);
setAndroidAttr(layout, ATTR_ORIENTATION, VALUE_HORIZONTAL);
setAndroidAttr(layout, ATTR_GRAVITY, GRAVITY_VALUE_CENTER_VERTICAL + "|" + GRAVITY_VALUE_RIGHT);
if (myApiLevel >= 11 && myResolver.getFrameworkResource(ResourceType.DRAWABLE, "action_bar_background") != null) {
setAndroidAttr(layout, ATTR_BACKGROUND, "@android:drawable/action_bar_background");
} else {
setAndroidAttr(layout, ATTR_BACKGROUND, "#ff85878a");
}
if (myApiLevel >= 11 && myResolver.getFrameworkResource(ResourceType.ATTR, "actionBarSize") != null) {
setAndroidAttr(layout, ATTR_LAYOUT_HEIGHT, "?android:attr/actionBarSize");
} else if (myResolver.getProjectResource(ResourceType.ATTR, "actionBarSize") != null) { // ActionBarCompat
setAndroidAttr(layout, ATTR_LAYOUT_HEIGHT, "?attr/actionBarSize");
} else {
setAndroidAttr(layout, ATTR_LAYOUT_HEIGHT, "48dp");
}
ManifestInfo manifestInfo = ManifestInfo.get(myModule);
String applicationIcon = manifestInfo.getApplicationIcon();
if (applicationIcon != null) {
Element imageView = myDocument.createElement(IMAGE_VIEW);
layout.appendChild(imageView);
setAndroidAttr(imageView, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
setAndroidAttr(imageView, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
setAndroidAttr(imageView, ATTR_SRC, applicationIcon);
setAndroidAttr(imageView, "scaleX", "0.9"); // HACK; find out what the real action bar does to the app icon
setAndroidAttr(imageView, "scaleY", "0.9");
Element dummy = myDocument.createElement(VIEW);
layout.appendChild(dummy);
setAndroidAttr(dummy, ATTR_LAYOUT_WEIGHT, "1");
setAndroidAttr(dummy, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
setAndroidAttr(dummy, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
}
return layout;
}
private Element createMenuPopup() {
Element layout = myDocument.createElement(LINEAR_LAYOUT);
setAndroidAttr(layout, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
setAndroidAttr(layout, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
setAndroidAttr(layout, ATTR_LAYOUT_GRAVITY, GRAVITY_VALUE_RIGHT);
setAndroidAttr(layout, ATTR_ORIENTATION, VALUE_VERTICAL);
if (myThemeIsLight) {
if (myApiLevel >= 11 && myResolver.getFrameworkResource(ResourceType.DRAWABLE, "menu_panel_holo_light") != null) {
setAndroidAttr(layout, ATTR_BACKGROUND, "@android:drawable/menu_panel_holo_light");
} else {
setAndroidAttr(layout, ATTR_BACKGROUND, "@android:drawable/popup_full_bright");
}
} else {
if (myApiLevel >= 11 && myResolver.getFrameworkResource(ResourceType.DRAWABLE, "menu_panel_holo_dark") != null) {
setAndroidAttr(layout, ATTR_BACKGROUND, "@android:drawable/menu_panel_holo_dark");
} else {
setAndroidAttr(layout, ATTR_BACKGROUND, "@android:drawable/popup_full_dark");
}
}
return layout;
}
private static void addFidelityWarning(Document document, Element root, String typeName) {
// Spacer: absorb all weight in the middle to push the warning view below all the way to the bottom
Element spacer = document.createElement(VIEW);
root.appendChild(spacer);
setAndroidAttr(spacer, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT);
setAndroidAttr(spacer, ATTR_LAYOUT_HEIGHT, VALUE_FILL_PARENT);
setAndroidAttr(spacer, ATTR_LAYOUT_WEIGHT, "1");
Element warningView = document.createElement(TEXT_VIEW);
root.appendChild(warningView);
setAndroidAttr(warningView, ATTR_TEXT, "(Note: " + typeName + " preview is only approximate)");
setAndroidAttr(warningView, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT);
setAndroidAttr(warningView, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
setAndroidAttr(warningView, ATTR_LAYOUT_MARGIN, "5dp");
setAndroidAttr(warningView, "textColor", "#ff0000");
setAndroidAttr(warningView, ATTR_GRAVITY, GRAVITY_VALUE_CENTER);
}
private boolean addActionBar(Element root) {
List<Pair<String,XmlTag>> icons = Lists.newArrayList();
if (myRootTag != null) {
for (XmlTag tag : myRootTag.getSubTags()) {
String icon = tag.getAttributeValue(ATTR_ICON, ANDROID_URI);
if (icon != null) {
icons.add(Pair.create(icon, tag));
if (icons.size() > 6) {
break;
}
}
}
}
// Fake action bar
if (!icons.isEmpty()) {
Element linear = createActionBar();
root.appendChild(linear);
for (Pair<String, XmlTag> pair : icons) {
String iconUrl = pair.getFirst();
XmlTag tag = pair.getSecond();
String showAsAction = tag.getAttributeValue(ATTR_SHOW_AS_ACTION, ANDROID_URI);
if (showAsAction == null) {
showAsAction = tag.getAttributeValue(ATTR_SHOW_AS_ACTION, AUTO_URI); // ActionBar compat
if (showAsAction == null) {
showAsAction = "";
}
}
if (showAsAction.contains(VALUE_NEVER)) {
continue;
}
Element imageView = myDocument.createElement(IMAGE_VIEW);
linear.appendChild(imageView);
setAndroidAttr(imageView, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
setAndroidAttr(imageView, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
setAndroidAttr(imageView, ATTR_SRC, iconUrl);
viewCookies.put(imageView, tag);
if (showAsAction.contains(VALUE_WITH_TEXT)) {
String title = tag.getAttributeValue(ATTR_TITLE, ANDROID_URI);
if (title != null) {
Element textView = myDocument.createElement(TEXT_VIEW);
linear.appendChild(textView);
setAndroidAttr(textView, ATTR_TEXT, title);
setAndroidAttr(textView, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
setAndroidAttr(textView, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
setAndroidAttr(textView, ATTR_LAYOUT_MARGIN_RIGHT, "8dp");
viewCookies.put(textView, tag);
}
} else {
setAndroidAttr(imageView, ATTR_LAYOUT_MARGIN_RIGHT, "8dp");
}
}
// More/Overflow button
// TODO: Dark vs light
myThemeIsLight = true; // Actionbar is always light here; we need to hardcode the background etc
String name = myThemeIsLight ? "ic_menu_moreoverflow_holo_light" : "ic_menu_moreoverflow_holo_dark";
// TODO: Also look for ic_menu_moreoverflow_holo_light
if (myApiLevel >= 11 && myResolver.getFrameworkResource(ResourceType.DRAWABLE, name) != null) {
Element imageView = myDocument.createElement(IMAGE_VIEW);
linear.appendChild(imageView);
setAndroidAttr(imageView, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
setAndroidAttr(imageView, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
setAndroidAttr(imageView, ATTR_SRC, "@android:drawable/" + name);
setAndroidAttr(imageView, ATTR_LAYOUT_MARGIN_RIGHT, "8dp");
}
return true;
}
return false;
}
private void populateMenu(@NotNull Element root) {
boolean dividerSupported = myApiLevel >= 11;
// The divider and dividerPadding attribute on LinearLayout doesn't work in layoutlib
// yet (see issue 29959) so use workaround of inserting individual <ImageView>
// items with a background corresponding to ?android:attr/dividerHorizontal instead?
boolean useDividerAttribute = false;
//noinspection ConstantConditions
if (dividerSupported && useDividerAttribute) {
setAndroidAttr(root, "divider", "?android:attr/actionBarDivider");
setAndroidAttr(root, "dividerPadding", "12dip");
}
String textNormalHeight;
if (myApiLevel >= 14 && myResolver.findResValue("?android:attr/dropdownListPreferredItemHeight", true) != null) {
textNormalHeight = "?android:attr/dropdownListPreferredItemHeight";
} else if (myApiLevel >= 14 && myResolver.findResValue("?android:attr/listPreferredItemHeightSmall", true) != null) {
textNormalHeight = "?android:attr/listPreferredItemHeightSmall";
} else {
textNormalHeight = "48dip";
}
boolean hasPopupTextAppearance = false;
if (myApiLevel >= 11 && myResolver.findResValue("?android:attr/textAppearanceLargePopupMenu", true) != null) {
hasPopupTextAppearance = true;
}
boolean first = root.getChildNodes().getLength() == 0;
List<MenuItem> items = readMenu();
for (MenuItem menuItem : items) {
XmlTag item = menuItem.tag;
if (!menuItem.visible) {
continue;
}
if (first) {
first = false;
} else //noinspection ConstantConditions
if (!useDividerAttribute) {
Element imageView = myDocument.createElement(IMAGE_VIEW);
root.appendChild(imageView);
setAndroidAttr(imageView, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT);
setAndroidAttr(imageView, ATTR_LAYOUT_HEIGHT, "1dp");
if (dividerSupported) {
setAndroidAttr(imageView, ATTR_BACKGROUND, "?android:attr/dividerHorizontal"); // instead of divider
}
}
// Make a ListMenuItemView (which is a LinearLayout) as configured in popup_menu_item_layout.xml
Element listMenuView = myDocument.createElement(LINEAR_LAYOUT);
root.appendChild(listMenuView);
viewCookies.put(listMenuView, item);
setAndroidAttr(listMenuView, ATTR_LAYOUT_WIDTH, VALUE_FILL_PARENT);
setAndroidAttr(listMenuView, ATTR_LAYOUT_HEIGHT, textNormalHeight);
setAndroidAttr(listMenuView, "minWidth", "196dip"); // From popup_menu_item_layout
setAndroidAttr(listMenuView, "paddingEnd", "16dip");
// TODO: Insert icon here?
// If so, prepend to listMenuView
// Depends on mMenu.getOptionalIconsVisible, off by default
//if (myApiLevel < 11) {
// String icon = item.getAttributeValue(ATTR_ICON, ANDROID_URI);
// if (icon != null) {
// setAndroidAttr(itemView, ATTR_DRAWABLE_LEFT, icon);
// }
//}
Element relative = myDocument.createElement(RELATIVE_LAYOUT);
listMenuView.appendChild(relative);
setAndroidAttr(relative, ATTR_LAYOUT_WIDTH, VALUE_ZERO_DP);
setAndroidAttr(relative, ATTR_LAYOUT_WEIGHT, VALUE_1);
setAndroidAttr(relative, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
setAndroidAttr(relative, ATTR_LAYOUT_GRAVITY, GRAVITY_VALUE_CENTER_VERTICAL);
setAndroidAttr(relative, ATTR_LAYOUT_MARGIN_LEFT, "16dip");
setAndroidAttr(relative, ATTR_DUPLICATE_PARENT_STATE, VALUE_TRUE);
Element itemView = myDocument.createElement(TEXT_VIEW);
relative.appendChild(itemView);
setAndroidAttr(itemView, ATTR_ID, "@+id/title");
setAndroidAttr(itemView, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
setAndroidAttr(itemView, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
setAndroidAttr(itemView, ATTR_LAYOUT_ALIGN_PARENT_TOP, VALUE_TRUE);
setAndroidAttr(itemView, ATTR_LAYOUT_ALIGN_PARENT_LEFT, VALUE_TRUE);
if (hasPopupTextAppearance) {
setAndroidAttr(itemView, ATTR_TEXT_APPEARANCE, "?android:attr/textAppearanceLargePopupMenu");
} else {
setAndroidAttr(itemView, ATTR_TEXT_SIZE, "22sp");
setAndroidAttr(itemView, ATTR_TEXT_COLOR, "?android:attr/textColorPrimary");
}
setAndroidAttr(itemView, ATTR_SINGLE_LINE, VALUE_TRUE);
setAndroidAttr(itemView, ATTR_DUPLICATE_PARENT_STATE, VALUE_TRUE);
if (myApiLevel >= 14) { // doesn't render on older version
setAndroidAttr(itemView, ATTR_ELLIPSIZE, VALUE_MARQUEE);
}
setAndroidAttr(itemView, ATTR_FADING_EDGE, "horizontal");
setAndroidAttr(itemView, ATTR_TEXT_ALIGNMENT, "viewStart");
String title = item.getAttributeValue(ATTR_TITLE, ANDROID_URI);
if (title != null) {
setAndroidAttr(itemView, ATTR_TEXT, title);
}
String visibility = item.getAttributeValue(ATTR_VISIBILITY, ANDROID_URI);
if (visibility != null) {
setAndroidAttr(itemView, ATTR_VISIBILITY, visibility);
}
if (!menuItem.enabled) {
setAndroidAttr(itemView, ATTR_ENABLED, VALUE_FALSE);
}
String shortcut = item.getAttributeValue(ATTR_ALPHABETIC_SHORTCUT, ANDROID_URI);
if (shortcut == null) {
shortcut = item.getAttributeValue(ATTR_NUMERIC_SHORTCUT, ANDROID_URI);
}
if (shortcut != null) {
Element shortCut = myDocument.createElement(TEXT_VIEW);
relative.appendChild(shortCut);
setAndroidAttr(shortCut, ATTR_TEXT, shortcut);
setAndroidAttr(shortCut, ATTR_ID, "@+id/shortcut");
setAndroidAttr(shortCut, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
setAndroidAttr(shortCut, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
setAndroidAttr(shortCut, ATTR_LAYOUT_BELOW, "@id/title");
setAndroidAttr(shortCut, ATTR_LAYOUT_ALIGN_PARENT_LEFT, VALUE_TRUE);
if (hasPopupTextAppearance) {
setAndroidAttr(shortCut, ATTR_TEXT_APPEARANCE, "?android:attr/textAppearanceSmallPopupMenu");
} else {
setAndroidAttr(shortCut, ATTR_TEXT_SIZE, "14sp");
setAndroidAttr(shortCut, ATTR_TEXT_COLOR, "?android:attr/textColorSecondary");
}
setAndroidAttr(shortCut, ATTR_SINGLE_LINE, VALUE_TRUE);
setAndroidAttr(shortCut, ATTR_DUPLICATE_PARENT_STATE, VALUE_TRUE);
setAndroidAttr(shortCut, ATTR_TEXT_ALIGNMENT, "viewStart");
if (visibility != null) {
setAndroidAttr(shortCut, ATTR_VISIBILITY, visibility);
}
}
// com.android.internal.view.menu.ListMenuItemView inserts a checkbox or radio button here
if (menuItem.checkable != 0) {
Element toggle = myDocument.createElement(menuItem.checkable == 1 ? CHECK_BOX : RADIO_BUTTON);
listMenuView.appendChild(toggle);
setAndroidAttr(toggle, ATTR_LAYOUT_WIDTH, VALUE_WRAP_CONTENT);
setAndroidAttr(toggle, ATTR_LAYOUT_HEIGHT, VALUE_WRAP_CONTENT);
setAndroidAttr(toggle, ATTR_LAYOUT_GRAVITY, GRAVITY_VALUE_CENTER_VERTICAL);
setAndroidAttr(toggle, ATTR_FOCUSABLE, VALUE_FALSE);
setAndroidAttr(toggle, ATTR_CLICKABLE, VALUE_FALSE);
setAndroidAttr(toggle, ATTR_DUPLICATE_PARENT_STATE, VALUE_TRUE);
String checked = item.getAttributeValue(ATTR_CHECKED, ANDROID_URI);
if (checked != null) {
toggle.setAttributeNS(ANDROID_URI, ATTR_CHECKED, checked);
}
if (!menuItem.enabled) {
setAndroidAttr(toggle, ATTR_ENABLED, VALUE_FALSE);
}
if (visibility != null) {
setAndroidAttr(toggle, ATTR_VISIBILITY, visibility);
}
}
}
}
private List<MenuItem> readMenu() {
ArrayList<MenuItem> items = Lists.newArrayList();
addMenuItems(items, myRootTag);
return items;
}
private void addMenuItems(ArrayList<MenuItem> items, XmlTag menuTag) {
for (XmlTag tag : menuTag.getSubTags()) {
String tagName = tag.getName();
if (TAG_ITEM.equals(tagName)) {
MenuItem item = readItem(tag);
items.add(findInsertIndex(items, item.ordering), item);
} else if (TAG_GROUP.equals(tagName)) {
readGroup(tag);
addMenuItems(items, tag);
resetGroup();
} else //noinspection StatementWithEmptyBody
if (TAG_MENU.equals(tagName)) {
// We don't need to process these at designtime; not rendering sub-menus (actionbar menus also do not give
// a visual indication of these)
}
}
}
private static boolean getBoolean(XmlTag tag, String attributeName, boolean defaultValue) {
String value = tag.getAttributeValue(attributeName, ANDROID_URI);
if (value != null) {
return Boolean.valueOf(value);
}
return defaultValue;
}
private static int getInt(XmlTag tag, String attributeName, int defaultValue) {
String value = tag.getAttributeValue(attributeName, ANDROID_URI);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
// fall through
}
}
return defaultValue;
}
private static int getCategory(XmlTag tag, int defaultValue) {
String category = tag.getAttributeValue(ATTR_MENU_CATEGORY, ANDROID_URI);
if (category != null) {
// Constants from attrs.xml: MenuGroup#menuCategory
if (category.equals("container")) {
return 0x00010000;
} else if (category.equals("system")) {
return 0x00020000;
} else if (category.equals("secondary")) {
return 0x00030000;
} else if (category.equals("alternative")) {
return 0x00040000;
}
}
return defaultValue;
}
private static final int CHECKABLE_NONE = 0;
private static final int CHECKABLE_ALL = 1;
private static final int CHECKABLE_EXCLUSIVE = 2;
/**
* A menu item. The tag field points to the XML attributes for the original item; all the other
* state pertains to ordering or inherited attributes from the surrounding group(s).
*/
private static final class MenuItem {
public final XmlTag tag;
public final int ordering;
public final boolean visible;
public final boolean enabled;
public final int checkable;
public MenuItem(XmlTag tag, int ordering, int checkable, boolean visible, boolean enabled) {
this.tag = tag;
this.ordering = ordering;
this.checkable = checkable;
this.visible = visible;
this.enabled = enabled;
}
}
// Condensed from android.view.MenuInflater.MenuState
private int myGroupCategory;
private int myGroupOrder;
private int myGroupCheckable;
private boolean myGroupVisible = true;
private boolean myGroupEnabled = true;
public void resetGroup() {
myGroupCategory = 0;
myGroupOrder = 0;
myGroupCheckable = 0;
myGroupVisible = true;
myGroupEnabled = true;
}
public void readGroup(XmlTag tag) {
assert tag.getName().equals(TAG_GROUP) : tag.getName();
myGroupCheckable = CHECKABLE_NONE;
String checkableBehavior = tag.getAttributeValue(ATTR_CHECKABLE_BEHAVIOR, ANDROID_URI);
if (VALUE_SINGLE.equals(checkableBehavior)) {
myGroupCheckable = CHECKABLE_ALL;
} else if (VALUE_ALL.equals(checkableBehavior)) {
myGroupCheckable = CHECKABLE_EXCLUSIVE;
} else {
myGroupCheckable = CHECKABLE_NONE;
}
myGroupCategory = getCategory(tag, 0);
myGroupOrder = getInt(tag, ATTR_ORDER_IN_CATEGORY, 0);
myGroupVisible = getBoolean(tag, ATTR_VISIBLE, true);
myGroupEnabled = getBoolean(tag, ATTR_ENABLED, true);
}
public MenuItem readItem(XmlTag tag) {
assert tag.getName().equals(TAG_ITEM) : tag.getName();
int category = getCategory(tag, myGroupCategory);
int order = getInt(tag, ATTR_ORDER_IN_CATEGORY, myGroupOrder);
boolean itemVisible = getBoolean(tag, ATTR_VISIBLE, myGroupVisible);
boolean itemEnabled = getBoolean(tag, ATTR_ENABLED, myGroupEnabled);
int itemCategoryOrder = (category & CATEGORY_MASK) | (order & USER_MASK);
XmlAttribute checkableAttribute = tag.getAttribute(ATTR_CHECKABLE, ANDROID_URI);
int itemCheckable;
if (checkableAttribute != null) {
itemCheckable = Boolean.valueOf(checkableAttribute.getValue()) ? 1 : 0;
} else {
itemCheckable = myGroupCheckable;
}
return new MenuItem(tag, getOrdering(itemCategoryOrder), itemCheckable, itemVisible, itemEnabled);
}
// Condensed from android.support.v7.internal.view.menu.MenuBuilder; code to handle ordering
// such that we end up with the same sort order as at runtime
private static int getOrdering(int categoryOrder) {
final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT;
assert index >= 0 && index < ourCategoryToOrder.length;
return (ourCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK);
}
private static int findInsertIndex(ArrayList<MenuItem> items, int ordering) {
for (int i = items.size() - 1; i >= 0; i--) {
MenuItem item = items.get(i);
if (item.ordering <= ordering) {
return i + 1;
}
}
return 0;
}
private static final int[] ourCategoryToOrder = new int[] {
1, /* No category */
4, /* CONTAINER */
5, /* SYSTEM */
3, /* SECONDARY */
2, /* ALTERNATIVE */
0, /* SELECTED_ALTERNATIVE */
};
private static final int USER_MASK = 0x0000ffff;
private static final int CATEGORY_MASK = 0xffff0000;
private static final int CATEGORY_SHIFT = 16;
}