Bug 785994: Submenu for custom menu. [r=mfinkle]
authorSriram Ramasubramanian <sriram@mozilla.com>
Wed, 19 Sep 2012 10:35:35 -0700
changeset 107626 8d44332e3c820e4e955e94741266a96f5b1633b4
parent 107625 8e7d9794e8356c46525ee639cb83257211ee1897
child 107627 a3992a7b52e30280637d2d55f889a6e36a47d45c
push id82
push usershu@rfrn.org
push dateFri, 05 Oct 2012 13:20:22 +0000
reviewersmfinkle
bugs785994
milestone18.0a1
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>