Bug 785994: Submenu for custom menu. [r=mfinkle]
authorSriram Ramasubramanian <sriram@mozilla.com>
Wed, 19 Sep 2012 10:35:35 -0700
changeset 107497 8d44332e3c820e4e955e94741266a96f5b1633b4
parent 107496 8e7d9794e8356c46525ee639cb83257211ee1897
child 107498 a3992a7b52e30280637d2d55f889a6e36a47d45c
push id15073
push usersramasubramanian@mozilla.com
push dateWed, 19 Sep 2012 17:43:11 +0000
treeherdermozilla-inbound@d71ef24d5b56 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs785994
milestone18.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 785994: Submenu for custom menu. [r=mfinkle]
mobile/android/base/BrowserApp.java
mobile/android/base/BrowserToolbar.java
mobile/android/base/GeckoApp.java
mobile/android/base/GeckoMenu.java
mobile/android/base/GeckoMenuInflater.java
mobile/android/base/GeckoMenuItem.java
mobile/android/base/GeckoSubMenu.java
mobile/android/base/Makefile.in
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/resources/menu-large-v11/browser_app_menu.xml.in
mobile/android/base/resources/menu-v11/browser_app_menu.xml.in
mobile/android/base/resources/menu-xlarge-v11/browser_app_menu.xml.in
mobile/android/base/strings.xml.in
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -711,17 +711,21 @@ abstract public class BrowserApp extends
             mMenuPanel.scrollTo(0, 0);
 
         if (!mBrowserToolbar.openOptionsMenu())
             super.openOptionsMenu();
     }
 
     @Override
     public void closeOptionsMenu() {
-        if (!mBrowserToolbar.closeOptionsMenu())
+        boolean closed = mBrowserToolbar.closeOptionsMenu();
+
+        if (closed)
+            onOptionsMenuClosed(mMenu);
+        else
             super.closeOptionsMenu();
     }
 
     @Override
     public void setFullScreen(final boolean fullscreen) {
       super.setFullScreen(fullscreen);
       mMainHandler.post(new Runnable() {
           public void run() {
--- a/mobile/android/base/BrowserToolbar.java
+++ b/mobile/android/base/BrowserToolbar.java
@@ -299,16 +299,22 @@ public class BrowserToolbar implements V
 
             if (panel == null) {
                 mActivity.onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null);
                 panel = mActivity.getMenuPanel();
 
                 if (mHasSoftMenuButton) {
                     mMenuPopup = new MenuPopup(mActivity);
                     mMenuPopup.setPanelView(panel);
+
+                    mMenuPopup.setOnDismissListener(new PopupWindow.OnDismissListener() {
+                        public void onDismiss() {
+                            mActivity.onOptionsMenuClosed(null);
+                        }
+                    });
                 }
             }
         }
 
         mFocusOrder = Arrays.asList(mBack, mForward, mAwesomeBar, mReader, mSiteSecurity, mStop, mTabs);
     }
 
     public View getLayout() {
@@ -691,17 +697,17 @@ public class BrowserToolbar implements V
 
         if (mMenuPopup != null && mMenuPopup.isShowing())
             mMenuPopup.dismiss();
 
         return true;
     }
 
     // MenuPopup holds the MenuPanel in Honeycomb/ICS devices with no hardware key
-    public class MenuPopup extends PopupWindow {
+    public static class MenuPopup extends PopupWindow {
         private RelativeLayout mPanel;
 
         public MenuPopup(Context context) {
             super(context);
             setFocusable(true);
 
             // Setting a null background makes the popup to not close on touching outside.
             setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -137,16 +137,17 @@ abstract public class GeckoApp
 
     StartupMode mStartupMode = null;
     protected LinearLayout mMainLayout;
     protected RelativeLayout mGeckoLayout;
     public View getView() { return mGeckoLayout; }
     public SurfaceView cameraView;
     public static GeckoApp mAppContext;
     public boolean mDOMFullScreen = false;
+    protected MenuPresenter mMenuPresenter;
     protected MenuPanel mMenuPanel;
     protected Menu mMenu;
     private static GeckoThread sGeckoThread;
     public Handler mMainHandler;
     private GeckoProfile mProfile;
     public static boolean sIsGeckoReady = false;
     public static int mOrientation;
     private boolean mIsRestoringActivity;
@@ -461,20 +462,24 @@ abstract public class GeckoApp
     @Override
     public MenuInflater getMenuInflater() {
         if (Build.VERSION.SDK_INT >= 11)
             return new GeckoMenuInflater(mAppContext);
         else
             return super.getMenuInflater();
     }
 
-    public View getMenuPanel() {
+    public MenuPanel getMenuPanel() {
         return mMenuPanel;
     }
 
+    public MenuPresenter getMenuPresenter() {
+        return mMenuPresenter;
+    }
+
     // MenuPanel holds the scrollable Menu
     public static class MenuPanel extends LinearLayout {
         public MenuPanel(Context context, AttributeSet attrs) {
             super(context, attrs);
             setLayoutParams(new ViewGroup.LayoutParams((int) context.getResources().getDimension(R.dimen.menu_item_row_width),
                                                        ViewGroup.LayoutParams.WRAP_CONTENT));
         }
 
@@ -492,21 +497,51 @@ abstract public class GeckoApp
         @Override
         public boolean dispatchPopulateAccessibilityEvent (AccessibilityEvent event) {
             if (Build.VERSION.SDK_INT >= 14) // Build.VERSION_CODES.ICE_CREAM_SANDWICH
                 onPopulateAccessibilityEvent(event);
             return true;
         }
     }
 
+    // MenuPresenter takes care of proper animation and inflation.
+    public class MenuPresenter {
+        GeckoApp mActivity;
+
+        public MenuPresenter(GeckoApp activity) {
+            mActivity = activity;
+        }
+
+        public void show(GeckoMenu menu) {
+            mActivity.closeOptionsMenu();
+
+            MenuPanel panel = mActivity.getMenuPanel();
+            panel.removeAllViews();
+            panel.addView(menu);
+
+            mActivity.openOptionsMenu();
+        }
+
+        public void hide() {
+            mActivity.closeOptionsMenu();
+        }
+
+        public void onOptionsMenuClosed() {
+            MenuPanel panel = mActivity.getMenuPanel();
+            panel.removeAllViews();
+            panel.addView((GeckoMenu) mMenu);
+        }
+    }
+
     @Override
     public View onCreatePanelView(int featureId) {
         if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) {
             if (mMenuPanel == null) {
                 mMenuPanel = new MenuPanel(mAppContext, null);
+                mMenuPresenter = new MenuPresenter(this);
             } else {
                 // Prepare the panel everytime before showing the menu.
                 onPreparePanel(featureId, mMenuPanel, mMenu);
             }
 
             return mMenuPanel; 
         }
   
@@ -568,16 +603,22 @@ abstract public class GeckoApp
                         System.exit(0);
                     sLaunchState = LaunchState.GeckoExiting;
                 }
                 return true;
             default:
                 return super.onOptionsItemSelected(item);
         }
     }
+
+    @Override
+    public void onOptionsMenuClosed(Menu menu) {
+        if (Build.VERSION.SDK_INT >= 11)
+            mMenuPresenter.onOptionsMenuClosed();
+    }
  
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         // Custom Menu should be opened when hardware menu key is pressed.
         if (Build.VERSION.SDK_INT >= 11 && keyCode == KeyEvent.KEYCODE_MENU) {
             openOptionsMenu();
             return true;
         }
--- a/mobile/android/base/GeckoMenu.java
+++ b/mobile/android/base/GeckoMenu.java
@@ -36,17 +36,17 @@ public class GeckoMenu extends ListView
     private Context mContext;
 
     public static interface ActionItemBarPresenter {
         public void addActionItem(View actionItem);
         public void removeActionItem(int index);
         public int getActionItemsCount();
     }
 
-    private static final int NO_ID = 0;
+    protected static final int NO_ID = 0;
 
     // List of all menu items.
     private List<GeckoMenuItem> mItems;
 
     // List of items in action-bar.
     private List<GeckoMenuItem> mActionItems;
 
     // Reference to action-items bar in action-bar.
@@ -55,17 +55,16 @@ public class GeckoMenu extends ListView
     // Adapter to hold the list of menu items.
     private MenuItemsAdapter mAdapter;
 
     // ActionBar to show the menu items as icons.
     private LinearLayout mActionBar;
 
     public GeckoMenu(Context context, AttributeSet attrs) {
         super(context, attrs);
-
         mContext = context;
 
         setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
                                          LayoutParams.FILL_PARENT));
 
         // Add a header view that acts as an action-bar.
         mActionBar = (LinearLayout) LayoutInflater.from(mContext).inflate(R.layout.menu_action_bar, null);
 
@@ -140,47 +139,69 @@ public class GeckoMenu extends ListView
 
     @Override
     public int addIntentOptions(int groupId, int itemId, int order, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) {
         return 0;
     }
 
     @Override
     public SubMenu addSubMenu(int groupId, int itemId, int order, CharSequence title) {
-        return null;
+        MenuItem menuItem = add(groupId, itemId, order, title);
+        GeckoSubMenu subMenu = new GeckoSubMenu(mContext, null);
+        subMenu.setMenuItem(menuItem);
+        ((GeckoMenuItem) menuItem).setSubMenu(subMenu);
+        return subMenu;
     }
 
     @Override
     public SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes) {
-        return null;
+        MenuItem menuItem = add(groupId, itemId, order, titleRes);
+        GeckoSubMenu subMenu = new GeckoSubMenu(mContext, null);
+        subMenu.setMenuItem(menuItem);
+        ((GeckoMenuItem) menuItem).setSubMenu(subMenu);
+        return subMenu;
     }
 
     @Override
     public SubMenu addSubMenu(CharSequence title) {
-        return null;
+        MenuItem menuItem = add(title);
+        GeckoSubMenu subMenu = new GeckoSubMenu(mContext, null);
+        subMenu.setMenuItem(menuItem);
+        ((GeckoMenuItem) menuItem).setSubMenu(subMenu);
+        return subMenu;
     }
 
     @Override
     public SubMenu addSubMenu(int titleRes) {
-       return null;
+        MenuItem menuItem = add(titleRes);
+        GeckoSubMenu subMenu = new GeckoSubMenu(mContext, null);
+        subMenu.setMenuItem(menuItem);
+        ((GeckoMenuItem) menuItem).setSubMenu(subMenu);
+        return subMenu;
     }
 
     @Override
     public void clear() {
     }
 
     @Override
     public void close() {
     }
 
     @Override
     public MenuItem findItem(int id) {
         for (GeckoMenuItem menuItem : mItems) {
-            if (menuItem.getItemId() == id)
+            if (menuItem.getItemId() == id) {
                 return menuItem;
+            } else if (menuItem.hasSubMenu()) {
+                SubMenu subMenu = menuItem.getSubMenu();
+                MenuItem item = subMenu.findItem(id);
+                if (item != null)
+                    return item;
+            }
         }
         return null;
     }
 
     @Override
     public MenuItem getItem(int index) {
         if (index < mItems.size())
             return mItems.get(index);
@@ -293,20 +314,28 @@ public class GeckoMenu extends ListView
         position -= getHeaderViewsCount();
         GeckoMenuItem item = mAdapter.getItem(position);
         if (item.isEnabled())
             item.onClick(item.getLayout());
     }
 
     @Override
     public boolean onMenuItemClick(MenuItem item) {
-        Activity activity = (Activity) mContext;
-        boolean result = activity.onOptionsItemSelected(item);
-        activity.closeOptionsMenu();
-        return result;
+        GeckoApp activity = (GeckoApp) mContext;
+
+        if (!item.hasSubMenu()) {
+            boolean result = activity.onOptionsItemSelected(item);
+            activity.closeOptionsMenu();
+            return result;
+        } else {
+            // Dismiss this menu.
+            GeckoApp.MenuPresenter presenter = activity.getMenuPresenter();
+            presenter.show((GeckoSubMenu) item.getSubMenu());
+            return true;
+        }
     }
 
     public void setActionItemBarPresenter(ActionItemBarPresenter presenter) {
         mActionItemBarPresenter = presenter;
     }
 
     // Action Items are added to the header view by default.
     // URL bar can register itself as a presenter, in case it has a different place to show them.
--- a/mobile/android/base/GeckoMenuInflater.java
+++ b/mobile/android/base/GeckoMenuInflater.java
@@ -11,78 +11,105 @@ import android.content.Context;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.util.AttributeSet;
 import android.util.Xml;
 import android.view.InflateException;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
+import android.view.SubMenu;
 
 import java.io.IOException;
 
 public class GeckoMenuInflater extends MenuInflater { 
     private static final String LOGTAG = "GeckoMenuInflater";
 
+    private static final String TAG_MENU = "menu";
     private static final String TAG_ITEM = "item";
     private static final int NO_ID = 0;
 
     private Context mContext;
 
+    private boolean isSubMenu;
+
     // Private class to hold the parsed menu item. 
     private class ParsedItem {
         public int id;
         public int order;
         public CharSequence title;
         public int iconRes;
         public boolean checkable;
         public boolean checked;
         public boolean visible;
         public boolean enabled;
         public boolean showAsAction;
     }
 
     public GeckoMenuInflater(Context context) {
         super(context);
         mContext = context;
+
+        isSubMenu = false;
     }
 
     public void inflate(int menuRes, Menu menu) {
 
-        // This is a very minimal parser for the custom menu.
-        // This assumes that there is only one menu tag in the resource file.
-        // This does not support sub-menus.
+        // This does not check for a well-formed XML.
 
         XmlResourceParser parser = null;
         try {
             parser = mContext.getResources().getXml(menuRes);
             AttributeSet attrs = Xml.asAttributeSet(parser);
 
             ParsedItem item = null;
+            SubMenu subMenu = null;
+            MenuItem menuItem = null;
    
             String tag;
             int eventType = parser.getEventType();
 
             do {
                 tag = parser.getName();
     
                 switch (eventType) {
                     case XmlPullParser.START_TAG:
                         if (tag.equals(TAG_ITEM)) {
                             // Parse the menu item.
                             item = new ParsedItem();
                             parseItem(item, attrs);
-                         }
+                         } else if (tag.equals(TAG_MENU)) {
+                            if (item != null) {
+                                // Start parsing the sub menu.
+                                isSubMenu = true;
+                                subMenu = menu.addSubMenu(NO_ID, item.id, item.order, item.title);
+                                menuItem = subMenu.getItem();
+
+                                // Set the menu item in main menu.
+                                setValues(item, menuItem);
+                            }
+                        }
                         break;
                         
                     case XmlPullParser.END_TAG:
                         if (parser.getName().equals(TAG_ITEM)) {
-                            // Add the item.
-                            MenuItem menuItem = menu.add(NO_ID, item.id, item.order, item.title);
-                            setValues(item, menuItem);
+                            if (isSubMenu && subMenu == null) {
+                                isSubMenu = false;
+                            } else {
+                                // Add the item.
+                                if (subMenu == null)
+                                    menuItem = menu.add(NO_ID, item.id, item.order, item.title);
+                                else
+                                    menuItem = subMenu.add(NO_ID, item.id, item.order, item.title);
+
+                                setValues(item, menuItem);
+                            }
+                        } else if (tag.equals(TAG_MENU)) {
+                            // End of sub menu.
+                            subMenu = null;
                         }
                         break;
                 }
 
                 eventType = parser.next();
 
             } while (eventType != XmlPullParser.END_DOCUMENT);
 
--- a/mobile/android/base/GeckoMenuItem.java
+++ b/mobile/android/base/GeckoMenuItem.java
@@ -46,16 +46,17 @@ public class GeckoMenuItem implements Me
     private CharSequence mTitle;
     private CharSequence mTitleCondensed;
     private boolean mCheckable;
     private boolean mChecked;
     private boolean mVisible;
     private boolean mEnabled;
     private Drawable mIcon;
     private int mIconRes;
+    private GeckoSubMenu mSubMenu;
     private MenuItem.OnMenuItemClickListener mMenuItemClickListener;
     private OnVisibilityChangedListener mVisibilityChangedListener;
     private OnShowAsActionChangedListener mShowAsActionChangedListener;
 
     public GeckoMenuItem(Context context, int id) {
         mContext = context;
         mLayout = new MenuItemDefault(context, null);
         mLayout.setId(id);
@@ -136,32 +137,32 @@ public class GeckoMenuItem implements Me
 
     @Override
     public int getOrder() {
         return mOrder;
     }
 
     @Override
     public SubMenu getSubMenu() {
-        return null;
+        return mSubMenu;
     }
 
     @Override
     public CharSequence getTitle() {
         return mTitle;
     }
 
     @Override
     public CharSequence getTitleCondensed() {
         return mTitleCondensed;
     }
 
     @Override
     public boolean hasSubMenu() {
-        return false;
+        return (mSubMenu != null);
     }
 
     @Override
     public boolean isActionViewExpanded() {
         return false;
     }
 
     @Override
@@ -303,16 +304,21 @@ public class GeckoMenuItem implements Me
         mShowAsActionChangedListener.onShowAsActionChanged(this, mActionItem);
     }
 
     @Override
     public MenuItem setShowAsActionFlags(int actionEnum) {
         return this;
     }
 
+    public MenuItem setSubMenu(GeckoSubMenu subMenu) {
+        mSubMenu = subMenu;
+        return this;
+    }
+
     @Override
     public MenuItem setTitle(CharSequence title) {
         mTitle = title;
         mLayout.setTitle(mTitle);
         return this;
     }
 
     @Override
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/GeckoSubMenu.java
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GeckoSubMenu extends GeckoMenu 
+                          implements SubMenu {
+    private static final String LOGTAG = "GeckoSubMenu";
+
+    private Context mContext;
+
+    // MenuItem associated with this submenu.
+    private MenuItem mMenuItem;
+
+    public GeckoSubMenu(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+    }
+
+    @Override
+    public void clearHeader() {
+    }
+
+    public SubMenu setMenuItem(MenuItem item) {
+        mMenuItem = item;
+        return this;
+    }
+
+    @Override
+    public MenuItem getItem() {
+        return mMenuItem;
+    }
+
+    @Override
+    public SubMenu setHeaderIcon(Drawable icon) {
+        return this;
+    }
+
+    @Override
+    public SubMenu setHeaderIcon(int iconRes) {
+        return this;
+    }
+
+    @Override
+    public SubMenu setHeaderTitle(CharSequence title) {
+        return this;
+    }
+
+    @Override
+    public SubMenu setHeaderTitle(int titleRes) {
+        return this;
+    }
+
+    @Override
+    public SubMenu setHeaderView(View view) { 
+        return this;
+    }
+
+    @Override
+    public SubMenu setIcon(Drawable icon) {
+        return this;
+    }
+
+    @Override
+    public SubMenu setIcon(int iconRes) {
+        return this;
+    }
+}
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -76,16 +76,17 @@ FENNEC_JAVA_FILES = \
   GeckoConnectivityReceiver.java \
   GeckoEvent.java \
   GeckoHalDefines.java \
   GeckoInputConnection.java \
   GeckoMenu.java \
   GeckoMenuInflater.java \
   GeckoMenuItem.java \
   GeckoMessageReceiver.java \
+  GeckoSubMenu.java \
   GeckoPreferences.java \
   GeckoProfile.java \
   GeckoThread.java \
   GlobalHistory.java \
   GeckoViewsFactory.java \
   InputMethods.java \
   LinkPreference.java \
   LinkTextView.java \
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -119,16 +119,17 @@ size. -->
 <!ENTITY char_encoding "Character Encoding">
 
 <!ENTITY share "Share">
 <!ENTITY share_title "Share via">
 <!ENTITY share_image_failed "Unable to share this image">
 <!ENTITY save_as_pdf "Save as PDF">
 <!ENTITY find_in_page "Find in Page">
 <!ENTITY desktop_mode "Request Desktop Site">
+<!ENTITY tools "Tools">
 
 <!-- Localization note (find_text, find_prev, find_next, find_close) : These strings are used
      as alternate text for accessibility. They are not visible in the UI. -->
 <!ENTITY find_text "Find in Page">
 <!ENTITY find_prev "Previous">
 <!ENTITY find_next "Next">
 <!ENTITY find_close "Close">
 
--- a/mobile/android/base/resources/menu-large-v11/browser_app_menu.xml.in
+++ b/mobile/android/base/resources/menu-large-v11/browser_app_menu.xml.in
@@ -23,37 +23,45 @@
     <item gecko:id="@+id/share"
           gecko:icon="@drawable/ic_menu_share"
           gecko:title="@string/share" /> 
     
     <item gecko:id="@+id/reading_list"
           gecko:icon="@drawable/ic_menu_reading_list_add"
           gecko:title="@string/reading_list" />
 
-    <item gecko:id="@+id/save_as_pdf"
-          gecko:icon="@drawable/ic_menu_save_as_pdf"
-          gecko:title="@string/save_as_pdf" />
-
     <item gecko:id="@+id/find_in_page"
           gecko:icon="@drawable/ic_menu_find_in_page"
           gecko:title="@string/find_in_page" />
 
     <item gecko:id="@+id/desktop_mode"
           gecko:icon="@drawable/ic_menu_desktop_mode"
           gecko:title="@string/desktop_mode"
           gecko:checkable="true" />
 
-    <item gecko:id="@+id/addons"
-          gecko:title="@string/addons"/>
+    <item gecko:title="@string/tools">
+
+        <menu>
+
+            <item gecko:id="@+id/save_as_pdf"
+                  gecko:icon="@drawable/ic_menu_save_as_pdf"
+                  gecko:title="@string/save_as_pdf" />
 
-    <item gecko:id="@+id/downloads"
-          gecko:title="@string/downloads"/>
+            <item gecko:id="@+id/addons"
+                  gecko:title="@string/addons"/>
+
+            <item gecko:id="@+id/downloads"
+                  gecko:title="@string/downloads"/>
 
-    <item gecko:id="@+id/apps"
-          gecko:title="@string/apps"/>
+            <item gecko:id="@+id/apps"
+                  gecko:title="@string/apps"/>
+
+        </menu>
+
+    </item>
 
     <item gecko:id="@+id/char_encoding"
           gecko:visible="false"
           gecko:title="@string/char_encoding"/>
 
     <item gecko:id="@+id/settings"
           gecko:title="@string/settings" />
 
--- a/mobile/android/base/resources/menu-v11/browser_app_menu.xml.in
+++ b/mobile/android/base/resources/menu-v11/browser_app_menu.xml.in
@@ -24,37 +24,45 @@
     <item gecko:id="@+id/share"
           gecko:icon="@drawable/ic_menu_share"
           gecko:title="@string/share" /> 
     
     <item gecko:id="@+id/reading_list"
           gecko:icon="@drawable/ic_menu_reading_list_add"
           gecko:title="@string/reading_list" />
 
-    <item gecko:id="@+id/save_as_pdf"
-          gecko:icon="@drawable/ic_menu_save_as_pdf"
-          gecko:title="@string/save_as_pdf" />
-
     <item gecko:id="@+id/find_in_page"
           gecko:icon="@drawable/ic_menu_find_in_page"
           gecko:title="@string/find_in_page" />
 
     <item gecko:id="@+id/desktop_mode"
           gecko:icon="@drawable/ic_menu_desktop_mode"
           gecko:title="@string/desktop_mode"
           gecko:checkable="true" />
 
-    <item gecko:id="@+id/addons"
-          gecko:title="@string/addons"/>
+    <item gecko:title="@string/tools">
+
+        <menu>
+
+            <item gecko:id="@+id/save_as_pdf"
+                  gecko:icon="@drawable/ic_menu_save_as_pdf"
+                  gecko:title="@string/save_as_pdf" />
 
-    <item gecko:id="@+id/downloads"
-          gecko:title="@string/downloads"/>
+            <item gecko:id="@+id/addons"
+                  gecko:title="@string/addons"/>
+
+            <item gecko:id="@+id/downloads"
+                  gecko:title="@string/downloads"/>
 
-    <item gecko:id="@+id/apps"
-          gecko:title="@string/apps"/>
+            <item gecko:id="@+id/apps"
+                  gecko:title="@string/apps"/>
+
+        </menu>
+
+    </item>
 
     <item gecko:id="@+id/char_encoding"
           gecko:visible="false"
           gecko:title="@string/char_encoding"/>
 
     <item gecko:id="@+id/settings"
           gecko:title="@string/settings" />
 
--- a/mobile/android/base/resources/menu-xlarge-v11/browser_app_menu.xml.in
+++ b/mobile/android/base/resources/menu-xlarge-v11/browser_app_menu.xml.in
@@ -24,37 +24,45 @@
     <item gecko:id="@+id/share"
           gecko:icon="@drawable/ic_menu_share"
           gecko:title="@string/share" /> 
     
     <item gecko:id="@+id/reading_list"
           gecko:icon="@drawable/ic_menu_reading_list_add"
           gecko:title="@string/reading_list" />
 
-    <item gecko:id="@+id/save_as_pdf"
-          gecko:icon="@drawable/ic_menu_save_as_pdf"
-          gecko:title="@string/save_as_pdf" />
-
     <item gecko:id="@+id/find_in_page"
           gecko:icon="@drawable/ic_menu_find_in_page"
           gecko:title="@string/find_in_page" />
 
     <item gecko:id="@+id/desktop_mode"
           gecko:icon="@drawable/ic_menu_desktop_mode"
           gecko:title="@string/desktop_mode"
           gecko:checkable="true" />
 
-    <item gecko:id="@+id/addons"
-          gecko:title="@string/addons"/>
+    <item gecko:title="@string/tools">
+
+        <menu>
+
+            <item gecko:id="@+id/save_as_pdf"
+                  gecko:icon="@drawable/ic_menu_save_as_pdf"
+                  gecko:title="@string/save_as_pdf" />
 
-    <item gecko:id="@+id/downloads"
-          gecko:title="@string/downloads"/>
+            <item gecko:id="@+id/addons"
+                  gecko:title="@string/addons"/>
+
+            <item gecko:id="@+id/downloads"
+                  gecko:title="@string/downloads"/>
 
-    <item gecko:id="@+id/apps"
-          gecko:title="@string/apps"/>
+            <item gecko:id="@+id/apps"
+                  gecko:title="@string/apps"/>
+
+        </menu>
+
+    </item>
 
     <item gecko:id="@+id/char_encoding"
           gecko:visible="false"
           gecko:title="@string/char_encoding"/>
 
     <item gecko:id="@+id/settings"
           gecko:title="@string/settings" />
 
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -55,16 +55,17 @@
   <string name="history_older_section">&history_older_section;</string>
 
   <string name="share">&share;</string>
   <string name="share_title">&share_title;</string>
   <string name="share_image_failed">&share_image_failed;</string>
   <string name="save_as_pdf">&save_as_pdf;</string>
   <string name="find_in_page">&find_in_page;</string>
   <string name="desktop_mode">&desktop_mode;</string>
+  <string name="tools">&tools;</string>
 
   <string name="find_text">&find_text;</string>
   <string name="find_prev">&find_prev;</string>
   <string name="find_next">&find_next;</string>
   <string name="find_close">&find_close;</string>
 
   <string name="settings">&settings;</string>
   <string name="settings_title">&settings_title;</string>