1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view; 18 19 import com.android.internal.view.menu.MenuItemImpl; 20 21 import java.io.IOException; 22 23 import org.xmlpull.v1.XmlPullParser; 24 import org.xmlpull.v1.XmlPullParserException; 25 26 import android.app.Activity; 27 import android.content.Context; 28 import android.content.res.TypedArray; 29 import android.content.res.XmlResourceParser; 30 import android.util.AttributeSet; 31 import android.util.Xml; 32 33 /** 34 * This class is used to instantiate menu XML files into Menu objects. 35 * <p> 36 * For performance reasons, menu inflation relies heavily on pre-processing of 37 * XML files that is done at build time. Therefore, it is not currently possible 38 * to use MenuInflater with an XmlPullParser over a plain XML file at runtime; 39 * it only works with an XmlPullParser returned from a compiled resource (R. 40 * <em>something</em> file.) 41 */ 42 public class MenuInflater { 43 /** Menu tag name in XML. */ 44 private static final String XML_MENU = "menu"; 45 46 /** Group tag name in XML. */ 47 private static final String XML_GROUP = "group"; 48 49 /** Item tag name in XML. */ 50 private static final String XML_ITEM = "item"; 51 52 private static final int NO_ID = 0; 53 54 private Context mContext; 55 56 /** 57 * Constructs a menu inflater. 58 * 59 * @see Activity#getMenuInflater() 60 */ MenuInflater(Context context)61 public MenuInflater(Context context) { 62 mContext = context; 63 } 64 65 /** 66 * Inflate a menu hierarchy from the specified XML resource. Throws 67 * {@link InflateException} if there is an error. 68 * 69 * @param menuRes Resource ID for an XML layout resource to load (e.g., 70 * <code>R.menu.main_activity</code>) 71 * @param menu The Menu to inflate into. The items and submenus will be 72 * added to this Menu. 73 */ inflate(int menuRes, Menu menu)74 public void inflate(int menuRes, Menu menu) { 75 XmlResourceParser parser = null; 76 try { 77 parser = mContext.getResources().getLayout(menuRes); 78 AttributeSet attrs = Xml.asAttributeSet(parser); 79 80 parseMenu(parser, attrs, menu); 81 } catch (XmlPullParserException e) { 82 throw new InflateException("Error inflating menu XML", e); 83 } catch (IOException e) { 84 throw new InflateException("Error inflating menu XML", e); 85 } finally { 86 if (parser != null) parser.close(); 87 } 88 } 89 90 /** 91 * Called internally to fill the given menu. If a sub menu is seen, it will 92 * call this recursively. 93 */ parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)94 private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu) 95 throws XmlPullParserException, IOException { 96 MenuState menuState = new MenuState(menu); 97 98 int eventType = parser.getEventType(); 99 String tagName; 100 boolean lookingForEndOfUnknownTag = false; 101 String unknownTagName = null; 102 103 // This loop will skip to the menu start tag 104 do { 105 if (eventType == XmlPullParser.START_TAG) { 106 tagName = parser.getName(); 107 if (tagName.equals(XML_MENU)) { 108 // Go to next tag 109 eventType = parser.next(); 110 break; 111 } 112 113 throw new RuntimeException("Expecting menu, got " + tagName); 114 } 115 eventType = parser.next(); 116 } while (eventType != XmlPullParser.END_DOCUMENT); 117 118 boolean reachedEndOfMenu = false; 119 while (!reachedEndOfMenu) { 120 switch (eventType) { 121 case XmlPullParser.START_TAG: 122 if (lookingForEndOfUnknownTag) { 123 break; 124 } 125 126 tagName = parser.getName(); 127 if (tagName.equals(XML_GROUP)) { 128 menuState.readGroup(attrs); 129 } else if (tagName.equals(XML_ITEM)) { 130 menuState.readItem(attrs); 131 } else if (tagName.equals(XML_MENU)) { 132 // A menu start tag denotes a submenu for an item 133 SubMenu subMenu = menuState.addSubMenuItem(); 134 135 // Parse the submenu into returned SubMenu 136 parseMenu(parser, attrs, subMenu); 137 } else { 138 lookingForEndOfUnknownTag = true; 139 unknownTagName = tagName; 140 } 141 break; 142 143 case XmlPullParser.END_TAG: 144 tagName = parser.getName(); 145 if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) { 146 lookingForEndOfUnknownTag = false; 147 unknownTagName = null; 148 } else if (tagName.equals(XML_GROUP)) { 149 menuState.resetGroup(); 150 } else if (tagName.equals(XML_ITEM)) { 151 // Add the item if it hasn't been added (if the item was 152 // a submenu, it would have been added already) 153 if (!menuState.hasAddedItem()) { 154 menuState.addItem(); 155 } 156 } else if (tagName.equals(XML_MENU)) { 157 reachedEndOfMenu = true; 158 } 159 break; 160 161 case XmlPullParser.END_DOCUMENT: 162 throw new RuntimeException("Unexpected end of document"); 163 } 164 165 eventType = parser.next(); 166 } 167 } 168 169 /** 170 * State for the current menu. 171 * <p> 172 * Groups can not be nested unless there is another menu (which will have 173 * its state class). 174 */ 175 private class MenuState { 176 private Menu menu; 177 178 /* 179 * Group state is set on items as they are added, allowing an item to 180 * override its group state. (As opposed to set on items at the group end tag.) 181 */ 182 private int groupId; 183 private int groupCategory; 184 private int groupOrder; 185 private int groupCheckable; 186 private boolean groupVisible; 187 private boolean groupEnabled; 188 189 private boolean itemAdded; 190 private int itemId; 191 private int itemCategoryOrder; 192 private String itemTitle; 193 private String itemTitleCondensed; 194 private int itemIconResId; 195 private char itemAlphabeticShortcut; 196 private char itemNumericShortcut; 197 /** 198 * Sync to attrs.xml enum: 199 * - 0: none 200 * - 1: all 201 * - 2: exclusive 202 */ 203 private int itemCheckable; 204 private boolean itemChecked; 205 private boolean itemVisible; 206 private boolean itemEnabled; 207 208 private static final int defaultGroupId = NO_ID; 209 private static final int defaultItemId = NO_ID; 210 private static final int defaultItemCategory = 0; 211 private static final int defaultItemOrder = 0; 212 private static final int defaultItemCheckable = 0; 213 private static final boolean defaultItemChecked = false; 214 private static final boolean defaultItemVisible = true; 215 private static final boolean defaultItemEnabled = true; 216 MenuState(final Menu menu)217 public MenuState(final Menu menu) { 218 this.menu = menu; 219 220 resetGroup(); 221 } 222 resetGroup()223 public void resetGroup() { 224 groupId = defaultGroupId; 225 groupCategory = defaultItemCategory; 226 groupOrder = defaultItemOrder; 227 groupCheckable = defaultItemCheckable; 228 groupVisible = defaultItemVisible; 229 groupEnabled = defaultItemEnabled; 230 } 231 232 /** 233 * Called when the parser is pointing to a group tag. 234 */ readGroup(AttributeSet attrs)235 public void readGroup(AttributeSet attrs) { 236 TypedArray a = mContext.obtainStyledAttributes(attrs, 237 com.android.internal.R.styleable.MenuGroup); 238 239 groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId); 240 groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory); 241 groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder); 242 groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable); 243 groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible); 244 groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled); 245 246 a.recycle(); 247 } 248 249 /** 250 * Called when the parser is pointing to an item tag. 251 */ readItem(AttributeSet attrs)252 public void readItem(AttributeSet attrs) { 253 TypedArray a = mContext.obtainStyledAttributes(attrs, 254 com.android.internal.R.styleable.MenuItem); 255 256 // Inherit attributes from the group as default value 257 itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId); 258 final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory); 259 final int order = a.getInt(com.android.internal.R.styleable.MenuItem_orderInCategory, groupOrder); 260 itemCategoryOrder = (category & Menu.CATEGORY_MASK) | (order & Menu.USER_MASK); 261 itemTitle = a.getString(com.android.internal.R.styleable.MenuItem_title); 262 itemTitleCondensed = a.getString(com.android.internal.R.styleable.MenuItem_titleCondensed); 263 itemIconResId = a.getResourceId(com.android.internal.R.styleable.MenuItem_icon, 0); 264 itemAlphabeticShortcut = 265 getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_alphabeticShortcut)); 266 itemNumericShortcut = 267 getShortcut(a.getString(com.android.internal.R.styleable.MenuItem_numericShortcut)); 268 if (a.hasValue(com.android.internal.R.styleable.MenuItem_checkable)) { 269 // Item has attribute checkable, use it 270 itemCheckable = a.getBoolean(com.android.internal.R.styleable.MenuItem_checkable, false) ? 1 : 0; 271 } else { 272 // Item does not have attribute, use the group's (group can have one more state 273 // for checkable that represents the exclusive checkable) 274 itemCheckable = groupCheckable; 275 } 276 itemChecked = a.getBoolean(com.android.internal.R.styleable.MenuItem_checked, defaultItemChecked); 277 itemVisible = a.getBoolean(com.android.internal.R.styleable.MenuItem_visible, groupVisible); 278 itemEnabled = a.getBoolean(com.android.internal.R.styleable.MenuItem_enabled, groupEnabled); 279 280 a.recycle(); 281 282 itemAdded = false; 283 } 284 getShortcut(String shortcutString)285 private char getShortcut(String shortcutString) { 286 if (shortcutString == null) { 287 return 0; 288 } else { 289 return shortcutString.charAt(0); 290 } 291 } 292 setItem(MenuItem item)293 private void setItem(MenuItem item) { 294 item.setChecked(itemChecked) 295 .setVisible(itemVisible) 296 .setEnabled(itemEnabled) 297 .setCheckable(itemCheckable >= 1) 298 .setTitleCondensed(itemTitleCondensed) 299 .setIcon(itemIconResId) 300 .setAlphabeticShortcut(itemAlphabeticShortcut) 301 .setNumericShortcut(itemNumericShortcut); 302 303 if (itemCheckable >= 2) { 304 ((MenuItemImpl) item).setExclusiveCheckable(true); 305 } 306 } 307 addItem()308 public void addItem() { 309 itemAdded = true; 310 setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle)); 311 } 312 addSubMenuItem()313 public SubMenu addSubMenuItem() { 314 itemAdded = true; 315 SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle); 316 setItem(subMenu.getItem()); 317 return subMenu; 318 } 319 hasAddedItem()320 public boolean hasAddedItem() { 321 return itemAdded; 322 } 323 } 324 325 } 326