MenuInflater.java revision 9066cfe9886ac131c34d59ed0e2d287b0e3c0087
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
17package android.view;
18
19import com.android.internal.view.menu.MenuItemImpl;
20
21import java.io.IOException;
22
23import org.xmlpull.v1.XmlPullParser;
24import org.xmlpull.v1.XmlPullParserException;
25
26import android.app.Activity;
27import android.content.Context;
28import android.content.res.TypedArray;
29import android.content.res.XmlResourceParser;
30import android.util.AttributeSet;
31import 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 */
42public 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     */
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     */
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     */
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
217        public MenuState(final Menu menu) {
218            this.menu = menu;
219
220            resetGroup();
221        }
222
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         */
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         */
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
285        private char getShortcut(String shortcutString) {
286            if (shortcutString == null) {
287                return 0;
288            } else {
289                return shortcutString.charAt(0);
290            }
291        }
292
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
308        public void addItem() {
309            itemAdded = true;
310            setItem(menu.add(groupId, itemId, itemCategoryOrder, itemTitle));
311        }
312
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
320        public boolean hasAddedItem() {
321            return itemAdded;
322        }
323    }
324
325}
326