--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -7,7 +7,12 @@
 # On macOS, we don't track whether activation of the native menubar happened
 # with the keyboard.
 #ifndef XP_MACOSX
-                onpopupshowing="if (event.target.parentNode.parentNode == this)
+                onpopupshowing="if (event.target.parentNode.parentNode == this &amp;&amp;
+#ifdef MOZ_WIDGET_GTK
+                                    document.documentElement.getAttribute('shellshowingmenubar') != 'true')
+#else
+                                    true)
+#endif
                                   this.setAttribute('openedwithkey',
                                                     event.target.parentNode.openedWithKey);"
 #endif
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6466,11 +6466,18 @@ function onViewToolbarsPopupShowing(aEve
   MozXULElement.insertFTLIfNeeded("browser/toolbarContextMenu.ftl");
   let firstMenuItem = aInsertPoint || popup.firstElementChild;
   let toolbarNodes = gNavToolbox.querySelectorAll("toolbar");
+
+  let shellShowingMenubar = document.documentElement.getAttribute("shellshowingmenubar") == "true";
+
   for (let toolbar of toolbarNodes) {
     if (!toolbar.hasAttribute("toolbarname")) {
       continue;
     }
 
+    if (shellShowingMenubar && toolbar.id == "toolbar-menubar") {
+      continue;
+    }
+
     if (toolbar.id == "PersonalToolbar") {
       let menu = BookmarkingUI.buildBookmarksToolbarSubmenu(toolbar);
       popup.insertBefore(menu, firstMenuItem);
--- a/browser/components/places/content/places.xhtml
+++ b/browser/components/places/content/places.xhtml
@@ -165,6 +165,7 @@
 #else
       <menubar id="placesMenu">
         <menu class="menu-iconic" data-l10n-id="places-organize-button"
+              _moz-menubarkeeplocal="true"
 #endif
               id="organizeButton">
           <menupopup id="organizeButtonPopup">
--- a/dom/xul/XULPopupElement.cpp
+++ b/dom/xul/XULPopupElement.cpp
@@ -208,6 +208,10 @@ void XULPopupElement::GetState(nsString&
   // set this here in case there's no frame for the popup
   aState.AssignLiteral("closed");
 
+#ifdef MOZ_WIDGET_GTK
+  nsAutoString nativeState;
+#endif
+
   if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
     switch (pm->GetPopupState(this)) {
       case ePopupShown:
@@ -230,6 +234,11 @@ void XULPopupElement::GetState(nsString&
         break;
     }
   }
+#ifdef MOZ_WIDGET_GTK
+  else if (GetAttr(kNameSpaceID_None, nsGkAtoms::_moz_nativemenupopupstate, nativeState)) {
+    aState = nativeState;
+  }
+#endif
 }
 
 nsINode* XULPopupElement::GetTriggerNode() const {
--- a/dom/xul/moz.build
+++ b/dom/xul/moz.build
@@ -82,4 +82,9 @@ LOCAL_INCLUDES += [
 
 include("/ipc/chromium/chromium-config.mozbuild")
 
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+    LOCAL_INCLUDES += [
+        "/widget/gtk",
+    ]
+
 FINAL_LIBRARY = "xul"
--- a/layout/build/moz.build
+++ b/layout/build/moz.build
@@ -70,6 +70,10 @@ elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "an
         "/dom/system",
         "/dom/system/android",
     ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+    LOCAL_INCLUDES += [
+        "/widget/gtk",
+    ]
 
 XPCOM_MANIFESTS += [
     "components.conf",
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -311,6 +311,9 @@ pref("dom.mouseevent.click.hack.use_lega
 // Fastback caching - if this pref is negative, then we calculate the number
 // of content viewers to cache based on the amount of available memory.
 pref("browser.sessionhistory.max_total_viewers", -1);
+#ifdef MOZ_WIDGET_GTK
+pref("ui.use_unity_menubar", true);
+#endif
 
 // min font device pixel size at which to turn on high quality
 pref("browser.display.auto_quality_min_font_size", 20);
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -229,6 +229,13 @@ toolbox {
   }
 }
 
+@media (-moz-platform: linux) {
+*|*:root[shellshowingmenubar="true"]
+toolbar[type="menubar"]:not([customizing="true"]) {
+  display: none !important;
+}
+}
+
 toolbarspring {
   -moz-box-flex: 1000;
 }
--- a/widget/gtk/moz.build
+++ b/widget/gtk/moz.build
@@ -77,6 +77,15 @@ UNIFIED_SOURCES += [
 
 SOURCES += [
     "MediaKeysEventSourceFactory.cpp",
+    "nsDbusmenu.cpp",
+    "nsMenu.cpp", # conflicts with X11 headers
+    "nsMenuBar.cpp",
+    "nsMenuContainer.cpp",
+    "nsMenuItem.cpp",
+    "nsMenuObject.cpp",
+    "nsMenuSeparator.cpp",
+    "nsNativeMenuDocListener.cpp",
+    "nsNativeMenuService.cpp",
     "nsNativeThemeGTK.cpp",  # conflicts with X11 headers
     "nsWindow.cpp",  # conflicts with X11 headers
     "WaylandVsyncSource.cpp",  # conflicts with X11 headers
@@ -152,6 +161,7 @@ LOCAL_INCLUDES += [
     "/layout/base",
     "/layout/forms",
     "/layout/generic",
+    "/layout/style",
     "/layout/xul",
     "/other-licenses/atk-1.0",
     "/third_party/cups/include",
--- /dev/null
+++ b/widget/gtk/nsDbusmenu.cpp
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#include "nsDbusmenu.h"
+#include "prlink.h"
+#include "mozilla/ArrayUtils.h"
+
+#define FUNC(name, type, params) \
+nsDbusmenuFunctions::_##name##_fn nsDbusmenuFunctions::s_##name;
+DBUSMENU_GLIB_FUNCTIONS
+DBUSMENU_GTK_FUNCTIONS
+#undef FUNC
+
+static PRLibrary *gDbusmenuGlib = nullptr;
+static PRLibrary *gDbusmenuGtk = nullptr;
+
+typedef void (*nsDbusmenuFunc)();
+struct nsDbusmenuDynamicFunction {
+    const char *functionName;
+    nsDbusmenuFunc *function;
+};
+
+/* static */ nsresult
+nsDbusmenuFunctions::Init()
+{
+#define FUNC(name, type, params) \
+    { #name, (nsDbusmenuFunc *)&nsDbusmenuFunctions::s_##name },
+    static const nsDbusmenuDynamicFunction kDbusmenuGlibSymbols[] = {
+        DBUSMENU_GLIB_FUNCTIONS
+    };
+    static const nsDbusmenuDynamicFunction kDbusmenuGtkSymbols[] = {
+        DBUSMENU_GTK_FUNCTIONS
+    };
+
+#define LOAD_LIBRARY(symbol, name) \
+    if (!g##symbol) { \
+        g##symbol = PR_LoadLibrary(name); \
+        if (!g##symbol) { \
+            return NS_ERROR_FAILURE; \
+        } \
+    } \
+    for (uint32_t i = 0; i < mozilla::ArrayLength(k##symbol##Symbols); ++i) { \
+        *k##symbol##Symbols[i].function = \
+            PR_FindFunctionSymbol(g##symbol, k##symbol##Symbols[i].functionName); \
+        if (!*k##symbol##Symbols[i].function) { \
+            return NS_ERROR_FAILURE; \
+        } \
+    }
+
+    LOAD_LIBRARY(DbusmenuGlib, "libdbusmenu-glib.so.4")
+#ifdef MOZ_WIDGET_GTK
+    LOAD_LIBRARY(DbusmenuGtk, "libdbusmenu-gtk3.so.4")
+#endif
+#undef LOAD_LIBRARY
+
+    return NS_OK;
+}
--- /dev/null
+++ b/widget/gtk/nsDbusmenu.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsDbusmenu_h__
+#define __nsDbusmenu_h__
+
+#include "nsError.h"
+
+#include <glib.h>
+#include <gdk/gdk.h>
+
+#define DBUSMENU_GLIB_FUNCTIONS \
+    FUNC(dbusmenu_menuitem_child_add_position, gboolean, (DbusmenuMenuitem *mi, DbusmenuMenuitem *child, guint position)) \
+    FUNC(dbusmenu_menuitem_child_append, gboolean, (DbusmenuMenuitem *mi, DbusmenuMenuitem *child)) \
+    FUNC(dbusmenu_menuitem_child_delete, gboolean, (DbusmenuMenuitem *mi, DbusmenuMenuitem *child)) \
+    FUNC(dbusmenu_menuitem_get_children, GList*, (DbusmenuMenuitem *mi)) \
+    FUNC(dbusmenu_menuitem_new, DbusmenuMenuitem*, (void)) \
+    FUNC(dbusmenu_menuitem_property_get, const gchar*, (DbusmenuMenuitem *mi, const gchar *property)) \
+    FUNC(dbusmenu_menuitem_property_get_bool, gboolean, (DbusmenuMenuitem *mi, const gchar *property)) \
+    FUNC(dbusmenu_menuitem_property_remove, void, (DbusmenuMenuitem *mi, const gchar *property)) \
+    FUNC(dbusmenu_menuitem_property_set, gboolean, (DbusmenuMenuitem *mi, const gchar *property, const gchar *value)) \
+    FUNC(dbusmenu_menuitem_property_set_bool, gboolean, (DbusmenuMenuitem *mi, const gchar *property, const gboolean value)) \
+    FUNC(dbusmenu_menuitem_property_set_int, gboolean, (DbusmenuMenuitem *mi, const gchar *property, const gint value)) \
+    FUNC(dbusmenu_menuitem_show_to_user, void, (DbusmenuMenuitem *mi, guint timestamp)) \
+    FUNC(dbusmenu_menuitem_take_children, GList*, (DbusmenuMenuitem *mi)) \
+    FUNC(dbusmenu_server_new, DbusmenuServer*, (const gchar *object)) \
+    FUNC(dbusmenu_server_set_root, void, (DbusmenuServer *server, DbusmenuMenuitem *root)) \
+    FUNC(dbusmenu_server_set_status, void, (DbusmenuServer *server, DbusmenuStatus status))
+
+#define DBUSMENU_GTK_FUNCTIONS \
+    FUNC(dbusmenu_menuitem_property_set_image, gboolean, (DbusmenuMenuitem *menuitem, const gchar *property, const GdkPixbuf *data)) \
+    FUNC(dbusmenu_menuitem_property_set_shortcut, gboolean, (DbusmenuMenuitem *menuitem, guint key, GdkModifierType modifier))
+
+typedef struct _DbusmenuMenuitem DbusmenuMenuitem;
+typedef struct _DbusmenuServer DbusmenuServer;
+
+enum DbusmenuStatus {
+    DBUSMENU_STATUS_NORMAL,
+    DBUSMENU_STATUS_NOTICE
+};
+
+#define DBUSMENU_MENUITEM_CHILD_DISPLAY_SUBMENU "submenu"
+#define DBUSMENU_MENUITEM_PROP_CHILD_DISPLAY "children-display"
+#define DBUSMENU_MENUITEM_PROP_ENABLED "enabled"
+#define DBUSMENU_MENUITEM_PROP_ICON_DATA "icon-data"
+#define DBUSMENU_MENUITEM_PROP_LABEL "label"
+#define DBUSMENU_MENUITEM_PROP_SHORTCUT "shortcut"
+#define DBUSMENU_MENUITEM_PROP_TYPE "type"
+#define DBUSMENU_MENUITEM_PROP_TOGGLE_STATE "toggle-state"
+#define DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE "toggle-type"
+#define DBUSMENU_MENUITEM_PROP_VISIBLE "visible"
+#define DBUSMENU_MENUITEM_SIGNAL_ABOUT_TO_SHOW "about-to-show"
+#define DBUSMENU_MENUITEM_SIGNAL_EVENT "event"
+#define DBUSMENU_MENUITEM_SIGNAL_ITEM_ACTIVATED "item-activated"
+#define DBUSMENU_MENUITEM_TOGGLE_CHECK "checkmark"
+#define DBUSMENU_MENUITEM_TOGGLE_RADIO "radio"
+#define DBUSMENU_MENUITEM_TOGGLE_STATE_CHECKED 1
+#define DBUSMENU_MENUITEM_TOGGLE_STATE_UNCHECKED 0
+#define DBUSMENU_SERVER_PROP_DBUS_OBJECT "dbus-object"
+
+class nsDbusmenuFunctions
+{
+public:
+    nsDbusmenuFunctions() = delete;
+
+    static nsresult Init();
+
+#define FUNC(name, type, params) \
+    typedef type (*_##name##_fn) params; \
+    static _##name##_fn s_##name;
+    DBUSMENU_GLIB_FUNCTIONS
+    DBUSMENU_GTK_FUNCTIONS
+#undef FUNC
+
+};
+
+#define dbusmenu_menuitem_child_add_position nsDbusmenuFunctions::s_dbusmenu_menuitem_child_add_position
+#define dbusmenu_menuitem_child_append nsDbusmenuFunctions::s_dbusmenu_menuitem_child_append
+#define dbusmenu_menuitem_child_delete nsDbusmenuFunctions::s_dbusmenu_menuitem_child_delete
+#define dbusmenu_menuitem_get_children nsDbusmenuFunctions::s_dbusmenu_menuitem_get_children
+#define dbusmenu_menuitem_new nsDbusmenuFunctions::s_dbusmenu_menuitem_new
+#define dbusmenu_menuitem_property_get nsDbusmenuFunctions::s_dbusmenu_menuitem_property_get
+#define dbusmenu_menuitem_property_get_bool nsDbusmenuFunctions::s_dbusmenu_menuitem_property_get_bool
+#define dbusmenu_menuitem_property_remove nsDbusmenuFunctions::s_dbusmenu_menuitem_property_remove
+#define dbusmenu_menuitem_property_set nsDbusmenuFunctions::s_dbusmenu_menuitem_property_set
+#define dbusmenu_menuitem_property_set_bool nsDbusmenuFunctions::s_dbusmenu_menuitem_property_set_bool
+#define dbusmenu_menuitem_property_set_int nsDbusmenuFunctions::s_dbusmenu_menuitem_property_set_int
+#define dbusmenu_menuitem_show_to_user nsDbusmenuFunctions::s_dbusmenu_menuitem_show_to_user
+#define dbusmenu_menuitem_take_children nsDbusmenuFunctions::s_dbusmenu_menuitem_take_children
+#define dbusmenu_server_new nsDbusmenuFunctions::s_dbusmenu_server_new
+#define dbusmenu_server_set_root nsDbusmenuFunctions::s_dbusmenu_server_set_root
+#define dbusmenu_server_set_status nsDbusmenuFunctions::s_dbusmenu_server_set_status
+
+#define dbusmenu_menuitem_property_set_image nsDbusmenuFunctions::s_dbusmenu_menuitem_property_set_image
+#define dbusmenu_menuitem_property_set_shortcut nsDbusmenuFunctions::s_dbusmenu_menuitem_property_set_shortcut
+
+#endif /* __nsDbusmenu_h__ */
--- /dev/null
+++ b/widget/gtk/nsMenu.cpp
@@ -0,0 +1,795 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#define _IMPL_NS_LAYOUT
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/ComputedStyleInlines.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/PresShellInlines.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsCSSValue.h"
+#include "nsGkAtoms.h"
+#include "nsGtkUtils.h"
+#include "nsAtom.h"
+#include "nsIContent.h"
+#include "nsIRunnable.h"
+#include "nsITimer.h"
+#include "nsString.h"
+#include "nsStyleStruct.h"
+#include "nsThreadUtils.h"
+
+#include "nsNativeMenuDocListener.h"
+
+#include <glib-object.h>
+
+#include "nsMenu.h"
+
+using namespace mozilla;
+
+class nsMenuContentInsertedEvent : public Runnable
+{
+public:
+    nsMenuContentInsertedEvent(nsMenu *aMenu,
+                               nsIContent *aContainer,
+                               nsIContent *aChild,
+                               nsIContent *aPrevSibling) :
+        Runnable("nsMenuContentInsertedEvent"),
+        mWeakMenu(aMenu),
+        mContainer(aContainer),
+        mChild(aChild),
+        mPrevSibling(aPrevSibling) { }
+
+    NS_IMETHODIMP Run()
+    {
+        if (!mWeakMenu) {
+            return NS_OK;
+        }
+
+        static_cast<nsMenu *>(mWeakMenu.get())->HandleContentInserted(mContainer,
+                                                                      mChild,
+                                                                      mPrevSibling);
+        return NS_OK;
+    }
+
+private:
+    nsWeakMenuObject mWeakMenu;
+
+    nsCOMPtr<nsIContent> mContainer;
+    nsCOMPtr<nsIContent> mChild;
+    nsCOMPtr<nsIContent> mPrevSibling;
+};
+
+class nsMenuContentRemovedEvent : public Runnable
+{
+public:
+    nsMenuContentRemovedEvent(nsMenu *aMenu,
+                              nsIContent *aContainer,
+                              nsIContent *aChild) :
+        Runnable("nsMenuContentRemovedEvent"),
+        mWeakMenu(aMenu),
+        mContainer(aContainer),
+        mChild(aChild) { }
+
+    NS_IMETHODIMP Run()
+    {
+        if (!mWeakMenu) {
+            return NS_OK;
+        }
+
+        static_cast<nsMenu *>(mWeakMenu.get())->HandleContentRemoved(mContainer,
+                                                                     mChild);
+        return NS_OK;
+    }
+
+private:
+    nsWeakMenuObject mWeakMenu;
+
+    nsCOMPtr<nsIContent> mContainer;
+    nsCOMPtr<nsIContent> mChild;
+};
+
+static void
+DispatchMouseEvent(nsIContent *aTarget, mozilla::EventMessage aMsg)
+{
+    if (!aTarget) {
+        return;
+    }
+
+    WidgetMouseEvent event(true, aMsg, nullptr, WidgetMouseEvent::eReal);
+    EventDispatcher::Dispatch(aTarget, nullptr, &event);
+}
+
+void
+nsMenu::SetPopupState(EPopupState aState)
+{
+    mPopupState = aState;
+
+    if (!mPopupContent) {
+        return;
+    }
+
+    nsAutoString state;
+    switch (aState) {
+        case ePopupState_Showing:
+            state.Assign(u"showing"_ns);
+            break;
+        case ePopupState_Open:
+            state.Assign(u"open"_ns);
+            break;
+        case ePopupState_Hiding:
+            state.Assign(u"hiding"_ns);
+            break;
+        default:
+            break;
+    }
+
+    if (state.IsEmpty()) {
+        mPopupContent->AsElement()->UnsetAttr(
+            kNameSpaceID_None, nsGkAtoms::_moz_nativemenupopupstate,
+            false);
+    } else {
+        mPopupContent->AsElement()->SetAttr(
+            kNameSpaceID_None, nsGkAtoms::_moz_nativemenupopupstate,
+            state, false);
+    }
+}
+
+/* static */ void
+nsMenu::DoOpenCallback(nsITimer *aTimer, void *aClosure)
+{
+    nsMenu* self = static_cast<nsMenu *>(aClosure);
+
+    dbusmenu_menuitem_show_to_user(self->GetNativeData(), 0);
+
+    self->mOpenDelayTimer = nullptr;
+}
+
+/* static */ void
+nsMenu::menu_event_cb(DbusmenuMenuitem *menu,
+                      const gchar *name,
+                      GVariant *value,
+                      guint timestamp,
+                      gpointer user_data)
+{
+    nsMenu *self = static_cast<nsMenu *>(user_data);
+
+    nsAutoCString event(name);
+
+    if (event.Equals("closed"_ns)) {
+        self->OnClose();
+        return;
+    }
+
+    if (event.Equals("opened"_ns)) {
+        self->OnOpen();
+        return;
+    }
+}
+
+void
+nsMenu::MaybeAddPlaceholderItem()
+{
+    MOZ_ASSERT(!IsInBatchedUpdate(),
+               "Shouldn't be modifying the native menu structure now");
+
+    GList *children = dbusmenu_menuitem_get_children(GetNativeData());
+    if (!children) {
+        MOZ_ASSERT(!mPlaceholderItem);
+
+        mPlaceholderItem = dbusmenu_menuitem_new();
+        if (!mPlaceholderItem) {
+            return;
+        }
+
+        dbusmenu_menuitem_property_set_bool(mPlaceholderItem,
+                                            DBUSMENU_MENUITEM_PROP_VISIBLE,
+                                            false);
+
+        MOZ_ALWAYS_TRUE(
+            dbusmenu_menuitem_child_append(GetNativeData(), mPlaceholderItem));
+    }
+}
+
+void
+nsMenu::EnsureNoPlaceholderItem()
+{
+    MOZ_ASSERT(!IsInBatchedUpdate(),
+               "Shouldn't be modifying the native menu structure now");
+
+    if (!mPlaceholderItem) {
+        return;
+    }
+
+    MOZ_ALWAYS_TRUE(
+        dbusmenu_menuitem_child_delete(GetNativeData(), mPlaceholderItem));
+    MOZ_ASSERT(!dbusmenu_menuitem_get_children(GetNativeData()));
+
+    g_object_unref(mPlaceholderItem);
+    mPlaceholderItem = nullptr;
+}
+
+void
+nsMenu::OnOpen()
+{
+    if (mNeedsRebuild) {
+        Build();
+    }
+
+    nsWeakMenuObject self(this);
+    nsCOMPtr<nsIContent> origPopupContent(mPopupContent);
+    {
+        nsNativeMenuDocListener::BlockUpdatesScope updatesBlocker;
+
+        SetPopupState(ePopupState_Showing);
+        DispatchMouseEvent(mPopupContent, eXULPopupShowing);
+
+        ContentNode()->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open,
+                                            u"true"_ns, true);
+    }
+
+    if (!self) {
+        // We were deleted!
+        return;
+    }
+
+    // I guess that the popup could have changed
+    if (origPopupContent != mPopupContent) {
+        return;
+    }
+
+    nsNativeMenuDocListener::BlockUpdatesScope updatesBlocker;
+
+    size_t count = ChildCount();
+    for (size_t i = 0; i < count; ++i) {
+        ChildAt(i)->ContainerIsOpening();
+    }
+
+    SetPopupState(ePopupState_Open);
+    DispatchMouseEvent(mPopupContent, eXULPopupShown);
+}
+
+void
+nsMenu::Build()
+{
+    mNeedsRebuild = false;
+
+    while (ChildCount() > 0) {
+        RemoveChildAt(0);
+    }
+
+    InitializePopup();
+
+    if (!mPopupContent) {
+        return;
+    }
+
+    uint32_t count = mPopupContent->GetChildCount();
+    for (uint32_t i = 0; i < count; ++i) {
+        nsIContent *childContent = mPopupContent->GetChildAt_Deprecated(i);
+
+        UniquePtr<nsMenuObject> child = CreateChild(childContent);
+
+        if (!child) {
+            continue;
+        }
+
+        AppendChild(std::move(child));
+    }
+}
+
+void
+nsMenu::InitializePopup()
+{
+    nsCOMPtr<nsIContent> oldPopupContent;
+    oldPopupContent.swap(mPopupContent);
+
+    for (uint32_t i = 0; i < ContentNode()->GetChildCount(); ++i) {
+        nsIContent *child = ContentNode()->GetChildAt_Deprecated(i);
+
+        if (child->NodeInfo()->NameAtom() == nsGkAtoms::menupopup) {
+            mPopupContent = child;
+            break;
+        }
+    }
+
+    if (oldPopupContent == mPopupContent) {
+        return;
+    }
+
+    // The popup has changed
+
+    if (oldPopupContent) {
+        DocListener()->UnregisterForContentChanges(oldPopupContent);
+    }
+
+    SetPopupState(ePopupState_Closed);
+
+    if (!mPopupContent) {
+        return;
+    }
+
+    DocListener()->RegisterForContentChanges(mPopupContent, this);
+}
+
+void
+nsMenu::RemoveChildAt(size_t aIndex)
+{
+    MOZ_ASSERT(IsInBatchedUpdate() || !mPlaceholderItem,
+               "Shouldn't have a placeholder menuitem");
+
+    nsMenuContainer::RemoveChildAt(aIndex, !IsInBatchedUpdate());
+    StructureMutated();
+
+    if (!IsInBatchedUpdate()) {
+        MaybeAddPlaceholderItem();
+    }
+}
+
+void
+nsMenu::RemoveChild(nsIContent *aChild)
+{
+    size_t index = IndexOf(aChild);
+    if (index == NoIndex) {
+        return;
+    }
+
+    RemoveChildAt(index);
+}
+
+void
+nsMenu::InsertChildAfter(UniquePtr<nsMenuObject> aChild,
+                         nsIContent *aPrevSibling)
+{
+    if (!IsInBatchedUpdate()) {
+        EnsureNoPlaceholderItem();
+    }
+
+    nsMenuContainer::InsertChildAfter(std::move(aChild), aPrevSibling,
+                                      !IsInBatchedUpdate());
+    StructureMutated();
+}
+
+void
+nsMenu::AppendChild(UniquePtr<nsMenuObject> aChild)
+{
+    if (!IsInBatchedUpdate()) {
+        EnsureNoPlaceholderItem();
+    }
+
+    nsMenuContainer::AppendChild(std::move(aChild), !IsInBatchedUpdate());
+    StructureMutated();
+}
+
+bool
+nsMenu::IsInBatchedUpdate() const
+{
+    return mBatchedUpdateState != eBatchedUpdateState_Inactive;
+}
+
+void
+nsMenu::StructureMutated()
+{
+    if (!IsInBatchedUpdate()) {
+        return;
+    }
+
+    mBatchedUpdateState = eBatchedUpdateState_DidMutate;
+}
+
+bool
+nsMenu::CanOpen() const
+{
+    bool isVisible = dbusmenu_menuitem_property_get_bool(GetNativeData(),
+                                                         DBUSMENU_MENUITEM_PROP_VISIBLE);
+    bool isDisabled = ContentNode()->AsElement()->AttrValueIs(kNameSpaceID_None,
+                                                              nsGkAtoms::disabled,
+                                                              nsGkAtoms::_true,
+                                                              eCaseMatters);
+
+    return (isVisible && !isDisabled);
+}
+
+void
+nsMenu::HandleContentInserted(nsIContent *aContainer,
+                              nsIContent *aChild,
+                              nsIContent *aPrevSibling)
+{
+    if (aContainer == mPopupContent) {
+        UniquePtr<nsMenuObject> child = CreateChild(aChild);
+
+        if (child) {
+            InsertChildAfter(std::move(child), aPrevSibling);
+        }
+    } else {
+        Build();
+    }
+}
+
+void
+nsMenu::HandleContentRemoved(nsIContent *aContainer, nsIContent *aChild)
+{
+    if (aContainer == mPopupContent) {
+        RemoveChild(aChild);
+    } else {
+        Build();
+    }
+}
+
+void
+nsMenu::InitializeNativeData()
+{
+    // Dbusmenu provides an "about-to-show" signal, and also "opened" and
+    // "closed" events. However, Unity is the only thing that sends
+    // both "about-to-show" and "opened" events. Unity 2D and the HUD only
+    // send "opened" events, so we ignore "about-to-show" (I don't think
+    // there's any real difference between them anyway).
+    // To complicate things, there are certain conditions where we don't
+    // get a "closed" event, so we need to be able to handle this :/
+    g_signal_connect(G_OBJECT(GetNativeData()), "event",
+                     G_CALLBACK(menu_event_cb), this);
+
+    mNeedsRebuild = true;
+    mNeedsUpdate = true;
+
+    MaybeAddPlaceholderItem();
+}
+
+void
+nsMenu::Update(const ComputedStyle *aComputedStyle)
+{
+    if (mNeedsUpdate) {
+        mNeedsUpdate = false;
+
+        UpdateLabel();
+        UpdateSensitivity();
+    }
+
+    UpdateVisibility(aComputedStyle);
+    UpdateIcon(aComputedStyle);
+}
+
+nsMenuObject::PropertyFlags
+nsMenu::SupportedProperties() const
+{
+    return static_cast<nsMenuObject::PropertyFlags>(
+        nsMenuObject::ePropLabel |
+        nsMenuObject::ePropEnabled |
+        nsMenuObject::ePropVisible |
+        nsMenuObject::ePropIconData |
+        nsMenuObject::ePropChildDisplay
+    );
+}
+
+void
+nsMenu::OnAttributeChanged(nsIContent *aContent, nsAtom *aAttribute)
+{
+    MOZ_ASSERT(aContent == ContentNode() || aContent == mPopupContent,
+               "Received an event that wasn't meant for us!");
+
+    if (mNeedsUpdate) {
+        return;
+    }
+
+    if (aContent != ContentNode()) {
+        return;
+    }
+
+    if (!Parent()->IsBeingDisplayed()) {
+        mNeedsUpdate = true;
+        return;
+    }
+
+    if (aAttribute == nsGkAtoms::disabled) {
+        UpdateSensitivity();
+    } else if (aAttribute == nsGkAtoms::label ||
+               aAttribute == nsGkAtoms::accesskey ||
+               aAttribute == nsGkAtoms::crop) {
+        UpdateLabel();
+    } else if (aAttribute == nsGkAtoms::hidden ||
+        aAttribute == nsGkAtoms::collapsed) {
+        RefPtr<const ComputedStyle> style = GetComputedStyle();
+        UpdateVisibility(style);
+    } else if (aAttribute == nsGkAtoms::image) {
+        RefPtr<const ComputedStyle> style = GetComputedStyle();
+        UpdateIcon(style);
+    }
+}
+
+void
+nsMenu::OnContentInserted(nsIContent *aContainer, nsIContent *aChild,
+                          nsIContent *aPrevSibling)
+{
+    MOZ_ASSERT(aContainer == ContentNode() || aContainer == mPopupContent,
+               "Received an event that wasn't meant for us!");
+
+    if (mNeedsRebuild) {
+        return;
+    }
+
+    if (mPopupState == ePopupState_Closed) {
+        mNeedsRebuild = true;
+        return;
+    }
+
+    nsContentUtils::AddScriptRunner(
+        new nsMenuContentInsertedEvent(this, aContainer, aChild,
+                                       aPrevSibling));
+}
+
+void
+nsMenu::OnContentRemoved(nsIContent *aContainer, nsIContent *aChild)
+{
+    MOZ_ASSERT(aContainer == ContentNode() || aContainer == mPopupContent,
+               "Received an event that wasn't meant for us!");
+
+    if (mNeedsRebuild) {
+        return;
+    }
+
+    if (mPopupState == ePopupState_Closed) {
+        mNeedsRebuild = true;
+        return;
+    }
+
+    nsContentUtils::AddScriptRunner(
+        new nsMenuContentRemovedEvent(this, aContainer, aChild));
+}
+
+/*
+ * Some menus (eg, the History menu in Firefox) refresh themselves on
+ * opening by removing all children and then re-adding new ones. As this
+ * happens whilst the menu is opening in Unity, it causes some flickering
+ * as the menu popup is resized multiple times. To avoid this, we try to
+ * reuse native menu items when the menu structure changes during a
+ * batched update. If we can handle menu structure changes from Gecko
+ * just by updating properties of native menu items (rather than destroying
+ * and creating new ones), then we eliminate any flickering that occurs as
+ * the menu is opened. To do this, we don't modify any native menu items
+ * until the end of the update batch.
+ */
+
+void
+nsMenu::OnBeginUpdates(nsIContent *aContent)
+{
+    MOZ_ASSERT(aContent == ContentNode() || aContent == mPopupContent,
+               "Received an event that wasn't meant for us!");
+    MOZ_ASSERT(!IsInBatchedUpdate(), "Already in an update batch!");
+
+    if (aContent != mPopupContent) {
+        return;
+    }
+
+    mBatchedUpdateState = eBatchedUpdateState_Active;
+}
+
+void
+nsMenu::OnEndUpdates()
+{
+    if (!IsInBatchedUpdate()) {
+        return;
+    }
+
+    bool didMutate = mBatchedUpdateState == eBatchedUpdateState_DidMutate;
+    mBatchedUpdateState = eBatchedUpdateState_Inactive;
+
+    /* Optimize for the case where we only had attribute changes */
+    if (!didMutate) {
+        return;
+    }
+
+    EnsureNoPlaceholderItem();
+
+    GList *nextNativeChild = dbusmenu_menuitem_get_children(GetNativeData());
+    DbusmenuMenuitem *nextOwnedNativeChild = nullptr;
+
+    size_t count = ChildCount();
+
+    // Find the first native menu item that is `owned` by a corresponding
+    // Gecko menuitem
+    for (size_t i = 0; i < count; ++i) {
+        if (ChildAt(i)->GetNativeData()) {
+            nextOwnedNativeChild = ChildAt(i)->GetNativeData();
+            break;
+        }
+    }
+
+    // Now iterate over all Gecko menuitems
+    for (size_t i = 0; i < count; ++i) {
+        nsMenuObject *child = ChildAt(i);
+
+        if (child->GetNativeData()) {
+            // This child already has a corresponding native menuitem.
+            // Remove all preceding orphaned native items. At this point, we
+            // modify the native menu structure.
+            while (nextNativeChild &&
+                   nextNativeChild->data != nextOwnedNativeChild) {
+
+                DbusmenuMenuitem *data =
+                    static_cast<DbusmenuMenuitem *>(nextNativeChild->data);
+                nextNativeChild = nextNativeChild->next;
+
+                MOZ_ALWAYS_TRUE(dbusmenu_menuitem_child_delete(GetNativeData(),
+                                                               data));
+            }
+
+            if (nextNativeChild) {
+                nextNativeChild = nextNativeChild->next;
+            }
+
+            // Now find the next native menu item that is `owned`
+            nextOwnedNativeChild = nullptr;
+            for (size_t j = i + 1; j < count; ++j) {
+                if (ChildAt(j)->GetNativeData()) {
+                    nextOwnedNativeChild = ChildAt(j)->GetNativeData();
+                    break;
+                }
+            }
+        } else {
+            // This child is new, and doesn't have a native menu item. Find one!
+            if (nextNativeChild &&
+                nextNativeChild->data != nextOwnedNativeChild) {
+
+                DbusmenuMenuitem *data =
+                    static_cast<DbusmenuMenuitem *>(nextNativeChild->data);
+
+                if (NS_SUCCEEDED(child->AdoptNativeData(data))) {
+                    nextNativeChild = nextNativeChild->next;
+                }
+            }
+
+            // There wasn't a suitable one available, so create a new one.
+            // At this point, we modify the native menu structure.
+            if (!child->GetNativeData()) {
+                child->CreateNativeData();
+                MOZ_ALWAYS_TRUE(
+                    dbusmenu_menuitem_child_add_position(GetNativeData(),
+                                                         child->GetNativeData(),
+                                                         i));
+            }
+        }
+    }
+
+    while (nextNativeChild) {
+        DbusmenuMenuitem *data =
+            static_cast<DbusmenuMenuitem *>(nextNativeChild->data);
+        nextNativeChild = nextNativeChild->next;
+
+        MOZ_ALWAYS_TRUE(dbusmenu_menuitem_child_delete(GetNativeData(), data));
+    }
+
+    MaybeAddPlaceholderItem();
+}
+
+nsMenu::nsMenu(nsMenuContainer *aParent, nsIContent *aContent) :
+    nsMenuContainer(aParent, aContent),
+    mNeedsRebuild(false),
+    mNeedsUpdate(false),
+    mPlaceholderItem(nullptr),
+    mPopupState(ePopupState_Closed),
+    mBatchedUpdateState(eBatchedUpdateState_Inactive)
+{
+    MOZ_COUNT_CTOR(nsMenu);
+}
+
+nsMenu::~nsMenu()
+{
+    if (IsInBatchedUpdate()) {
+        OnEndUpdates();
+    }
+
+    // Although nsTArray will take care of this in its destructor,
+    // we have to manually ensure children are removed from our native menu
+    // item, just in case our parent recycles us
+    while (ChildCount() > 0) {
+        RemoveChildAt(0);
+    }
+
+    EnsureNoPlaceholderItem();
+
+    if (DocListener() && mPopupContent) {
+        DocListener()->UnregisterForContentChanges(mPopupContent);
+    }
+
+    if (GetNativeData()) {
+        g_signal_handlers_disconnect_by_func(GetNativeData(),
+                                             FuncToGpointer(menu_event_cb),
+                                             this);
+    }
+
+    MOZ_COUNT_DTOR(nsMenu);
+}
+
+nsMenuObject::EType
+nsMenu::Type() const
+{
+    return eType_Menu;
+}
+
+bool
+nsMenu::IsBeingDisplayed() const
+{
+    return mPopupState == ePopupState_Open;
+}
+
+bool
+nsMenu::NeedsRebuild() const
+{
+    return mNeedsRebuild;
+}
+
+void
+nsMenu::OpenMenu()
+{
+    if (!CanOpen()) {
+        return;
+    }
+
+    if (mOpenDelayTimer) {
+        return;
+    }
+
+    // Here, we synchronously fire popupshowing and popupshown events and then
+    // open the menu after a short delay. This allows the menu to refresh before
+    // it's shown, and avoids an issue where keyboard focus is not on the first
+    // item of the history menu in Firefox when opening it with the keyboard,
+    // because extra items to appear at the top of the menu
+
+    OnOpen();
+
+    mOpenDelayTimer = NS_NewTimer();
+    if (!mOpenDelayTimer) {
+        return;
+    }
+
+    if (NS_FAILED(mOpenDelayTimer->InitWithNamedFuncCallback(DoOpenCallback,
+                                                             this,
+                                                             100,
+                                                             nsITimer::TYPE_ONE_SHOT,
+                                                             "nsMenu::DoOpenCallback"))) {
+        mOpenDelayTimer = nullptr;
+    }
+}
+
+void
+nsMenu::OnClose()
+{
+    if (mPopupState == ePopupState_Closed) {
+        return;
+    }
+
+    MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
+
+    // We do this to avoid mutating our view of the menu until
+    // after we have finished
+    nsNativeMenuDocListener::BlockUpdatesScope updatesBlocker;
+
+    SetPopupState(ePopupState_Hiding);
+    DispatchMouseEvent(mPopupContent, eXULPopupHiding);
+
+    // Sigh, make sure all of our descendants are closed, as we don't
+    // always get closed events for submenus when scrubbing quickly through
+    // the menu
+    size_t count = ChildCount();
+    for (size_t i = 0; i < count; ++i) {
+        if (ChildAt(i)->Type() == nsMenuObject::eType_Menu) {
+            static_cast<nsMenu *>(ChildAt(i))->OnClose();
+        }
+    }
+
+    SetPopupState(ePopupState_Closed);
+    DispatchMouseEvent(mPopupContent, eXULPopupHidden);
+
+    ContentNode()->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::open,
+                                          true);
+}
+
--- /dev/null
+++ b/widget/gtk/nsMenu.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsMenu_h__
+#define __nsMenu_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+
+#include "nsDbusmenu.h"
+#include "nsMenuContainer.h"
+#include "nsMenuObject.h"
+
+#include <glib.h>
+
+class nsAtom;
+class nsIContent;
+class nsITimer;
+
+#define NSMENU_NUMBER_OF_POPUPSTATE_BITS 2U
+#define NSMENU_NUMBER_OF_FLAGS           4U
+
+// This class represents a menu
+class nsMenu final : public nsMenuContainer
+{
+public:
+    nsMenu(nsMenuContainer *aParent, nsIContent *aContent);
+    ~nsMenu();
+
+    nsMenuObject::EType Type() const override;
+
+    bool IsBeingDisplayed() const override;
+    bool NeedsRebuild() const override;
+
+    // Tell the desktop shell to display this menu
+    void OpenMenu();
+
+    // Normally called via the shell, but it's public so that child
+    // menuitems can do the shells work. Sigh....
+    void OnClose();
+
+private:
+    friend class nsMenuContentInsertedEvent;
+    friend class nsMenuContentRemovedEvent;
+
+    enum EPopupState {
+        ePopupState_Closed,
+        ePopupState_Showing,
+        ePopupState_Open,
+        ePopupState_Hiding
+    };
+
+    void SetPopupState(EPopupState aState);
+
+    static void DoOpenCallback(nsITimer *aTimer, void *aClosure);
+    static void menu_event_cb(DbusmenuMenuitem *menu,
+                              const gchar *name,
+                              GVariant *value,
+                              guint timestamp,
+                              gpointer user_data);
+
+    // We add a placeholder item to empty menus so that Unity actually treats
+    // us as a proper menu, rather than a menuitem without a submenu
+    void MaybeAddPlaceholderItem();
+
+    // Removes a placeholder item if it exists and asserts that this succeeds
+    void EnsureNoPlaceholderItem();
+
+    void OnOpen();
+    void Build();
+    void InitializePopup();
+    void RemoveChildAt(size_t aIndex);
+    void RemoveChild(nsIContent *aChild);
+    void InsertChildAfter(mozilla::UniquePtr<nsMenuObject> aChild,
+                          nsIContent *aPrevSibling);
+    void AppendChild(mozilla::UniquePtr<nsMenuObject> aChild);
+    bool IsInBatchedUpdate() const;
+    void StructureMutated();
+    bool CanOpen() const;
+
+    void HandleContentInserted(nsIContent *aContainer,
+                               nsIContent *aChild,
+                               nsIContent *aPrevSibling);
+    void HandleContentRemoved(nsIContent *aContainer,
+                              nsIContent *aChild);
+
+    void InitializeNativeData() override;
+    void Update(const mozilla::ComputedStyle *aComputedStyle) override;
+    nsMenuObject::PropertyFlags SupportedProperties() const override;
+
+    void OnAttributeChanged(nsIContent *aContent, nsAtom *aAttribute) override;
+    void OnContentInserted(nsIContent *aContainer, nsIContent *aChild,
+                           nsIContent *aPrevSibling) override;
+    void OnContentRemoved(nsIContent *aContainer, nsIContent *aChild) override;
+    void OnBeginUpdates(nsIContent *aContent) override;
+    void OnEndUpdates() override;
+
+    bool mNeedsRebuild;
+    bool mNeedsUpdate;
+
+    DbusmenuMenuitem *mPlaceholderItem;
+
+    EPopupState mPopupState;
+
+    enum EBatchedUpdateState {
+        eBatchedUpdateState_Inactive,
+        eBatchedUpdateState_Active,
+        eBatchedUpdateState_DidMutate
+    };
+
+    EBatchedUpdateState mBatchedUpdateState;
+
+    nsCOMPtr<nsIContent> mPopupContent;
+
+    nsCOMPtr<nsITimer> mOpenDelayTimer;
+};
+
+#endif /* __nsMenu_h__ */
--- /dev/null
+++ b/widget/gtk/nsMenuBar.cpp
@@ -0,0 +1,548 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#include "mozilla/Assertions.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/KeyboardEvent.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include "mozilla/Preferences.h"
+#include "nsContentUtils.h"
+#include "nsIDOMEventListener.h"
+#include "nsIRunnable.h"
+#include "nsIWidget.h"
+#include "nsTArray.h"
+#include "nsUnicharUtils.h"
+
+#include "nsMenu.h"
+#include "nsNativeMenuService.h"
+
+#include <gdk/gdk.h>
+#include <gdk/gdkx.h>
+#include <glib.h>
+#include <glib-object.h>
+
+#include "nsMenuBar.h"
+
+using namespace mozilla;
+
+static bool
+ShouldHandleKeyEvent(dom::KeyboardEvent *aEvent)
+{
+    return !aEvent->DefaultPrevented() && aEvent->IsTrusted();
+}
+
+class nsMenuBarContentInsertedEvent : public Runnable
+{
+public:
+    nsMenuBarContentInsertedEvent(nsMenuBar *aMenuBar,
+                                  nsIContent *aChild,
+                                  nsIContent *aPrevSibling) :
+        Runnable("nsMenuBarContentInsertedEvent"),
+        mWeakMenuBar(aMenuBar),
+        mChild(aChild),
+        mPrevSibling(aPrevSibling) { }
+
+    NS_IMETHODIMP Run()
+    {
+        if (!mWeakMenuBar) {
+            return NS_OK;
+        }
+
+        static_cast<nsMenuBar *>(mWeakMenuBar.get())->HandleContentInserted(mChild,
+                                                                            mPrevSibling);
+        return NS_OK;
+    }
+
+private:
+    nsWeakMenuObject mWeakMenuBar;
+
+    nsCOMPtr<nsIContent> mChild;
+    nsCOMPtr<nsIContent> mPrevSibling;
+};
+
+class nsMenuBarContentRemovedEvent : public Runnable
+{
+public:
+    nsMenuBarContentRemovedEvent(nsMenuBar *aMenuBar,
+                                 nsIContent *aChild) :
+        Runnable("nsMenuBarContentRemovedEvent"),
+        mWeakMenuBar(aMenuBar),
+        mChild(aChild) { }
+
+    NS_IMETHODIMP Run()
+    {
+        if (!mWeakMenuBar) {
+            return NS_OK;
+        }
+
+        static_cast<nsMenuBar *>(mWeakMenuBar.get())->HandleContentRemoved(mChild);
+        return NS_OK;
+    }
+
+private:
+    nsWeakMenuObject mWeakMenuBar;
+
+    nsCOMPtr<nsIContent> mChild;
+};
+
+class nsMenuBar::DocEventListener final : public nsIDOMEventListener
+{
+public:
+    NS_DECL_ISUPPORTS
+    NS_DECL_NSIDOMEVENTLISTENER
+
+    DocEventListener(nsMenuBar *aOwner) : mOwner(aOwner) { };
+
+private:
+    ~DocEventListener() { };
+
+    nsMenuBar *mOwner;
+};
+
+NS_IMPL_ISUPPORTS(nsMenuBar::DocEventListener, nsIDOMEventListener)
+
+NS_IMETHODIMP
+nsMenuBar::DocEventListener::HandleEvent(dom::Event *aEvent)
+{
+    nsAutoString type;
+    aEvent->GetType(type);
+
+    if (type.Equals(u"focus"_ns)) {
+        mOwner->Focus();
+    } else if (type.Equals(u"blur"_ns)) {
+        mOwner->Blur();
+    }
+
+    RefPtr<dom::KeyboardEvent> keyEvent = aEvent->AsKeyboardEvent();
+    if (!keyEvent) {
+        return NS_OK;
+    }
+
+    if (type.Equals(u"keypress"_ns)) {
+        return mOwner->Keypress(keyEvent);
+    } else if (type.Equals(u"keydown"_ns)) {
+        return mOwner->KeyDown(keyEvent);
+    } else if (type.Equals(u"keyup"_ns)) {
+        return mOwner->KeyUp(keyEvent);
+    }
+
+    return NS_OK;
+}
+
+nsMenuBar::nsMenuBar(nsIContent *aMenuBarNode) :
+    nsMenuContainer(new nsNativeMenuDocListener(aMenuBarNode), aMenuBarNode),
+    mTopLevel(nullptr),
+    mServer(nullptr),
+    mIsActive(false)
+{
+    MOZ_COUNT_CTOR(nsMenuBar);
+}
+
+nsresult
+nsMenuBar::Init(nsIWidget *aParent)
+{
+    MOZ_ASSERT(aParent);
+
+    GdkWindow *gdkWin = static_cast<GdkWindow *>(
+        aParent->GetNativeData(NS_NATIVE_WINDOW));
+    if (!gdkWin) {
+        return NS_ERROR_FAILURE;
+    }
+
+    gpointer user_data = nullptr;
+    gdk_window_get_user_data(gdkWin, &user_data);
+    if (!user_data || !GTK_IS_CONTAINER(user_data)) {
+        return NS_ERROR_FAILURE;
+    }
+
+    mTopLevel = gtk_widget_get_toplevel(GTK_WIDGET(user_data));
+    if (!mTopLevel) {
+        return NS_ERROR_FAILURE;
+    }
+
+    g_object_ref(mTopLevel);
+
+    nsAutoCString path;
+    path.Append("/com/canonical/menu/"_ns);
+    char xid[10];
+    sprintf(xid, "%X", static_cast<uint32_t>(
+        GDK_WINDOW_XID(gtk_widget_get_window(mTopLevel))));
+    path.Append(xid);
+
+    mServer = dbusmenu_server_new(path.get());
+    if (!mServer) {
+        return NS_ERROR_FAILURE;
+    }
+
+    CreateNativeData();
+    if (!GetNativeData()) {
+        return NS_ERROR_FAILURE;
+    }
+
+    dbusmenu_server_set_root(mServer, GetNativeData());
+
+    mEventListener = new DocEventListener(this);
+
+    mDocument = ContentNode()->OwnerDoc();
+
+    mAccessKey = Preferences::GetInt("ui.key.menuAccessKey");
+    if (mAccessKey == dom::KeyboardEvent_Binding::DOM_VK_SHIFT) {
+        mAccessKeyMask = eModifierShift;
+    } else if (mAccessKey == dom::KeyboardEvent_Binding::DOM_VK_CONTROL) {
+        mAccessKeyMask = eModifierCtrl;
+    } else if (mAccessKey == dom::KeyboardEvent_Binding::DOM_VK_ALT) {
+        mAccessKeyMask = eModifierAlt;
+    } else if (mAccessKey == dom::KeyboardEvent_Binding::DOM_VK_META) {
+        mAccessKeyMask = eModifierMeta;
+    } else {
+        mAccessKeyMask = eModifierAlt;
+    }
+
+    return NS_OK;
+}
+
+void
+nsMenuBar::Build()
+{
+    uint32_t count = ContentNode()->GetChildCount();
+    for (uint32_t i = 0; i < count; ++i) {
+        nsIContent *childContent = ContentNode()->GetChildAt_Deprecated(i);
+
+        UniquePtr<nsMenuObject> child = CreateChild(childContent);
+
+        if (!child) {
+            continue;
+        }
+
+        AppendChild(std::move(child));
+    }
+}
+
+void
+nsMenuBar::DisconnectDocumentEventListeners()
+{
+    mDocument->RemoveEventListener(u"focus"_ns,
+                                   mEventListener,
+                                   true);
+    mDocument->RemoveEventListener(u"blur"_ns,
+                                   mEventListener,
+                                   true);
+    mDocument->RemoveEventListener(u"keypress"_ns,
+                                   mEventListener,
+                                   false);
+    mDocument->RemoveEventListener(u"keydown"_ns,
+                                   mEventListener,
+                                   false);
+    mDocument->RemoveEventListener(u"keyup"_ns,
+                                   mEventListener,
+                                   false);
+}
+
+void
+nsMenuBar::SetShellShowingMenuBar(bool aShowing)
+{
+    ContentNode()->OwnerDoc()->GetRootElement()->SetAttr(
+        kNameSpaceID_None, nsGkAtoms::shellshowingmenubar,
+        aShowing ? u"true"_ns : u"false"_ns,
+        true);
+}
+
+void
+nsMenuBar::Focus()
+{
+    ContentNode()->AsElement()->SetAttr(kNameSpaceID_None,
+                                        nsGkAtoms::openedwithkey,
+                                        u"false"_ns, true);
+}
+
+void
+nsMenuBar::Blur()
+{
+    // We do this here in case we lose focus before getting the
+    // keyup event, which leaves the menubar state looking like
+    // the alt key is stuck down
+    dbusmenu_server_set_status(mServer, DBUSMENU_STATUS_NORMAL);
+}
+
+nsMenuBar::ModifierFlags
+nsMenuBar::GetModifiersFromEvent(dom::KeyboardEvent *aEvent)
+{
+    ModifierFlags modifiers = static_cast<ModifierFlags>(0);
+
+    if (aEvent->AltKey()) {
+        modifiers = static_cast<ModifierFlags>(modifiers | eModifierAlt);
+    }
+
+    if (aEvent->ShiftKey()) {
+        modifiers = static_cast<ModifierFlags>(modifiers | eModifierShift);
+    }
+
+    if (aEvent->CtrlKey()) {
+        modifiers = static_cast<ModifierFlags>(modifiers | eModifierCtrl);
+    }
+
+    if (aEvent->MetaKey()) {
+        modifiers = static_cast<ModifierFlags>(modifiers | eModifierMeta);
+    }
+
+    return modifiers;
+}
+
+nsresult
+nsMenuBar::Keypress(dom::KeyboardEvent *aEvent)
+{
+    if (!ShouldHandleKeyEvent(aEvent)) {
+        return NS_OK;
+    }
+
+    ModifierFlags modifiers = GetModifiersFromEvent(aEvent);
+    if (((modifiers & mAccessKeyMask) == 0) ||
+        ((modifiers & ~mAccessKeyMask) != 0)) {
+        return NS_OK;
+    }
+
+    uint32_t charCode = aEvent->CharCode();
+    if (charCode == 0) {
+        return NS_OK;
+    }
+
+    char16_t ch = char16_t(charCode);
+    char16_t chl = ToLowerCase(ch);
+    char16_t chu = ToUpperCase(ch);
+
+    nsMenuObject *found = nullptr;
+    uint32_t count = ChildCount();
+    for (uint32_t i = 0; i < count; ++i) {
+        nsAutoString accesskey;
+        ChildAt(i)->ContentNode()->AsElement()->GetAttr(kNameSpaceID_None,
+                                                        nsGkAtoms::accesskey,
+                                                        accesskey);
+        const nsAutoString::char_type *key = accesskey.BeginReading();
+        if (*key == chu || *key == chl) {
+            found = ChildAt(i);
+            break;
+        }
+    }
+
+    if (!found || found->Type() != nsMenuObject::eType_Menu) {
+        return NS_OK;
+    }
+
+    ContentNode()->AsElement()->SetAttr(kNameSpaceID_None,
+                                        nsGkAtoms::openedwithkey,
+                                        u"true"_ns, true);
+    static_cast<nsMenu *>(found)->OpenMenu();
+
+    aEvent->StopPropagation();
+    aEvent->PreventDefault();
+
+    return NS_OK;
+}
+
+nsresult
+nsMenuBar::KeyDown(dom::KeyboardEvent *aEvent)
+{
+    if (!ShouldHandleKeyEvent(aEvent)) {
+        return NS_OK;
+    }
+
+    uint32_t keyCode = aEvent->KeyCode();
+    ModifierFlags modifiers = GetModifiersFromEvent(aEvent);
+    if ((keyCode != mAccessKey) || ((modifiers & ~mAccessKeyMask) != 0)) {
+        return NS_OK;
+    }
+
+    dbusmenu_server_set_status(mServer, DBUSMENU_STATUS_NOTICE);
+
+    return NS_OK;
+}
+
+nsresult
+nsMenuBar::KeyUp(dom::KeyboardEvent *aEvent)
+{
+    if (!ShouldHandleKeyEvent(aEvent)) {
+        return NS_OK;
+    }
+
+    uint32_t keyCode = aEvent->KeyCode();
+    if (keyCode == mAccessKey) {
+        dbusmenu_server_set_status(mServer, DBUSMENU_STATUS_NORMAL);
+    }
+
+    return NS_OK;
+}
+
+void
+nsMenuBar::HandleContentInserted(nsIContent *aChild, nsIContent *aPrevSibling)
+{
+    UniquePtr<nsMenuObject> child = CreateChild(aChild);
+
+    if (!child) {
+        return;
+    }
+
+    InsertChildAfter(std::move(child), aPrevSibling);
+}
+
+void
+nsMenuBar::HandleContentRemoved(nsIContent *aChild)
+{
+    RemoveChild(aChild);
+}
+
+void
+nsMenuBar::OnContentInserted(nsIContent *aContainer, nsIContent *aChild,
+                             nsIContent *aPrevSibling)
+{
+    MOZ_ASSERT(aContainer == ContentNode(),
+               "Received an event that wasn't meant for us");
+
+    nsContentUtils::AddScriptRunner(
+        new nsMenuBarContentInsertedEvent(this, aChild, aPrevSibling));
+}
+
+void
+nsMenuBar::OnContentRemoved(nsIContent *aContainer, nsIContent *aChild)
+{
+    MOZ_ASSERT(aContainer == ContentNode(),
+               "Received an event that wasn't meant for us");
+
+    nsContentUtils::AddScriptRunner(
+        new nsMenuBarContentRemovedEvent(this, aChild));
+}
+
+nsMenuBar::~nsMenuBar()
+{
+    nsNativeMenuService *service = nsNativeMenuService::GetSingleton();
+    if (service) {
+        service->NotifyNativeMenuBarDestroyed(this);
+    }
+
+    if (ContentNode()) {
+        SetShellShowingMenuBar(false);
+    }
+
+    // We want to destroy all children before dropping our reference
+    // to the doc listener
+    while (ChildCount() > 0) {
+        RemoveChildAt(0);
+    }
+
+    if (mTopLevel) {
+        g_object_unref(mTopLevel);
+    }
+
+    if (DocListener()) {
+        DocListener()->Stop();
+    }
+
+    if (mDocument) {
+        DisconnectDocumentEventListeners();
+    }
+
+    if (mServer) {
+        g_object_unref(mServer);
+    }
+
+    MOZ_COUNT_DTOR(nsMenuBar);
+}
+
+/* static */ UniquePtr<nsMenuBar>
+nsMenuBar::Create(nsIWidget *aParent, nsIContent *aMenuBarNode)
+{
+    UniquePtr<nsMenuBar> menubar(new nsMenuBar(aMenuBarNode));
+    if (NS_FAILED(menubar->Init(aParent))) {
+        return nullptr;
+    }
+
+    return menubar;
+}
+
+nsMenuObject::EType
+nsMenuBar::Type() const
+{
+    return eType_MenuBar;
+}
+
+bool
+nsMenuBar::IsBeingDisplayed() const
+{
+    return true;
+}
+
+uint32_t
+nsMenuBar::WindowId() const
+{
+    return static_cast<uint32_t>(GDK_WINDOW_XID(gtk_widget_get_window(mTopLevel)));
+}
+
+nsCString
+nsMenuBar::ObjectPath() const
+{
+    gchar *tmp;
+    g_object_get(mServer, DBUSMENU_SERVER_PROP_DBUS_OBJECT, &tmp, NULL);
+
+    nsCString result;
+    result.Adopt(tmp);
+
+    return result;
+}
+
+void
+nsMenuBar::Activate()
+{
+    if (mIsActive) {
+        return;
+    }
+
+    mIsActive = true;
+
+    mDocument->AddEventListener(u"focus"_ns,
+                                mEventListener,
+                                true);
+    mDocument->AddEventListener(u"blur"_ns,
+                                mEventListener,
+                                true);
+    mDocument->AddEventListener(u"keypress"_ns,
+                                mEventListener,
+                                false);
+    mDocument->AddEventListener(u"keydown"_ns,
+                                mEventListener,
+                                false);
+    mDocument->AddEventListener(u"keyup"_ns,
+                                mEventListener,
+                                false);
+
+    // Clear this. Not sure if we really need to though
+    ContentNode()->AsElement()->SetAttr(kNameSpaceID_None,
+                                        nsGkAtoms::openedwithkey,
+                                        u"false"_ns, true);
+
+    DocListener()->Start();
+    Build();
+    SetShellShowingMenuBar(true);
+}
+
+void
+nsMenuBar::Deactivate()
+{
+    if (!mIsActive) {
+        return;
+    }
+
+    mIsActive = false;
+
+    SetShellShowingMenuBar(false);
+    while (ChildCount() > 0) {
+        RemoveChildAt(0);
+    }
+    DocListener()->Stop();
+    DisconnectDocumentEventListeners();
+}
--- /dev/null
+++ b/widget/gtk/nsMenuBar.h
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsMenuBar_h__
+#define __nsMenuBar_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+#include "nsDbusmenu.h"
+#include "nsMenuContainer.h"
+#include "nsMenuObject.h"
+
+#include <gtk/gtk.h>
+
+class nsIContent;
+class nsIWidget;
+class nsMenuBarDocEventListener;
+
+namespace mozilla {
+namespace dom {
+class Document;
+class KeyboardEvent;
+}
+}
+
+/*
+ * The menubar class. There is one of these per window (and the window
+ * owns its menubar). Each menubar has an object path, and the service is
+ * responsible for telling the desktop shell which object path corresponds
+ * to a particular window. A menubar and its hierarchy also own a
+ * nsNativeMenuDocListener.
+ */
+class nsMenuBar final : public nsMenuContainer
+{
+public:
+    ~nsMenuBar() override;
+
+    static mozilla::UniquePtr<nsMenuBar> Create(nsIWidget *aParent,
+                                                nsIContent *aMenuBarNode);
+
+    nsMenuObject::EType Type() const override;
+
+    bool IsBeingDisplayed() const override;
+
+    // Get the native window ID for this menubar
+    uint32_t WindowId() const;
+
+    // Get the object path for this menubar
+    nsCString ObjectPath() const;
+
+    // Get the top-level GtkWindow handle
+    GtkWidget* TopLevelWindow() { return mTopLevel; }
+
+    // Called from the menuservice when the menubar is about to be registered.
+    // Causes the native menubar to be created, and the XUL menubar to be hidden
+    void Activate();
+
+    // Called from the menuservice when the menubar is no longer registered
+    // with the desktop shell. Will cause the XUL menubar to be shown again
+    void Deactivate();
+
+private:
+    class DocEventListener;
+    friend class nsMenuBarContentInsertedEvent;
+    friend class nsMenuBarContentRemovedEvent;
+
+    enum ModifierFlags {
+        eModifierShift = (1 << 0),
+        eModifierCtrl = (1 << 1),
+        eModifierAlt = (1 << 2),
+        eModifierMeta = (1 << 3)
+    };
+
+    nsMenuBar(nsIContent *aMenuBarNode);
+    nsresult Init(nsIWidget *aParent);
+    void Build();
+    void DisconnectDocumentEventListeners();
+    void SetShellShowingMenuBar(bool aShowing);
+    void Focus();
+    void Blur();
+    ModifierFlags GetModifiersFromEvent(mozilla::dom::KeyboardEvent *aEvent);
+    nsresult Keypress(mozilla::dom::KeyboardEvent *aEvent);
+    nsresult KeyDown(mozilla::dom::KeyboardEvent *aEvent);
+    nsresult KeyUp(mozilla::dom::KeyboardEvent *aEvent);
+
+    void HandleContentInserted(nsIContent *aChild,
+                               nsIContent *aPrevSibling);
+    void HandleContentRemoved(nsIContent *aChild);
+
+    void OnContentInserted(nsIContent *aContainer, nsIContent *aChild,
+                           nsIContent *aPrevSibling) override;
+    void OnContentRemoved(nsIContent *aContainer, nsIContent *aChild) override;
+
+    GtkWidget *mTopLevel;
+    DbusmenuServer *mServer;
+    nsCOMPtr<mozilla::dom::Document> mDocument;
+    RefPtr<DocEventListener> mEventListener;
+
+    uint32_t mAccessKey;
+    ModifierFlags mAccessKeyMask;
+    bool mIsActive;
+};
+
+#endif /* __nsMenuBar_h__ */
--- /dev/null
+++ b/widget/gtk/nsMenuContainer.cpp
@@ -0,0 +1,170 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#include "mozilla/DebugOnly.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+
+#include "nsDbusmenu.h"
+#include "nsMenu.h"
+#include "nsMenuItem.h"
+#include "nsMenuSeparator.h"
+
+#include "nsMenuContainer.h"
+
+using namespace mozilla;
+
+const nsMenuContainer::ChildTArray::index_type nsMenuContainer::NoIndex = nsMenuContainer::ChildTArray::NoIndex;
+
+typedef UniquePtr<nsMenuObject> (*nsMenuObjectConstructor)(nsMenuContainer*,
+                                                           nsIContent*);
+
+template<class T>
+static UniquePtr<nsMenuObject> CreateMenuObject(nsMenuContainer *aContainer,
+                                                nsIContent *aContent)
+{
+    return UniquePtr<T>(new T(aContainer, aContent));
+}
+
+static nsMenuObjectConstructor
+GetMenuObjectConstructor(nsIContent *aContent)
+{
+    if (aContent->IsXULElement(nsGkAtoms::menuitem)) {
+        return CreateMenuObject<nsMenuItem>;
+    } else if (aContent->IsXULElement(nsGkAtoms::menu)) {
+        return CreateMenuObject<nsMenu>;
+    } else if (aContent->IsXULElement(nsGkAtoms::menuseparator)) {
+        return CreateMenuObject<nsMenuSeparator>;
+    }
+
+    return nullptr;
+}
+
+static bool
+ContentIsSupported(nsIContent *aContent)
+{
+    return GetMenuObjectConstructor(aContent) ? true : false;
+}
+
+nsMenuContainer::nsMenuContainer(nsMenuContainer *aParent,
+                                 nsIContent *aContent) :
+    nsMenuObject(aParent, aContent)
+{
+}
+
+nsMenuContainer::nsMenuContainer(nsNativeMenuDocListener *aListener,
+                                 nsIContent *aContent) :
+    nsMenuObject(aListener, aContent)
+{
+}
+
+UniquePtr<nsMenuObject>
+nsMenuContainer::CreateChild(nsIContent *aContent)
+{
+    nsMenuObjectConstructor ctor = GetMenuObjectConstructor(aContent);
+    if (!ctor) {
+        // There are plenty of node types we might stumble across that
+        // aren't supported
+        return nullptr;
+    }
+
+    UniquePtr<nsMenuObject> res = ctor(this, aContent);
+    return res;
+}
+
+size_t
+nsMenuContainer::IndexOf(nsIContent *aChild) const
+{
+    if (!aChild) {
+        return NoIndex;
+    }
+
+    size_t count = ChildCount();
+    for (size_t i = 0; i < count; ++i) {
+        if (ChildAt(i)->ContentNode() == aChild) {
+            return i;
+        }
+    }
+
+    return NoIndex;
+}
+
+void
+nsMenuContainer::RemoveChildAt(size_t aIndex, bool aUpdateNative)
+{
+    MOZ_ASSERT(aIndex < ChildCount());
+
+    if (aUpdateNative) {
+        MOZ_ALWAYS_TRUE(
+            dbusmenu_menuitem_child_delete(GetNativeData(),
+                                           ChildAt(aIndex)->GetNativeData()));
+    }
+
+    mChildren.RemoveElementAt(aIndex);
+}
+
+void
+nsMenuContainer::RemoveChild(nsIContent *aChild, bool aUpdateNative)
+{
+    size_t index = IndexOf(aChild);
+    if (index == NoIndex) {
+        return;
+    }
+
+    RemoveChildAt(index, aUpdateNative);
+}
+
+void
+nsMenuContainer::InsertChildAfter(UniquePtr<nsMenuObject> aChild,
+                                  nsIContent *aPrevSibling,
+                                  bool aUpdateNative)
+{
+    size_t index = IndexOf(aPrevSibling);
+    MOZ_ASSERT(!aPrevSibling || index != NoIndex);
+
+    ++index;
+
+    if (aUpdateNative) {
+        aChild->CreateNativeData();
+        MOZ_ALWAYS_TRUE(
+            dbusmenu_menuitem_child_add_position(GetNativeData(),
+                                                 aChild->GetNativeData(),
+                                                 index));
+    }
+
+    mChildren.InsertElementAt(index, std::move(aChild));
+}
+
+void
+nsMenuContainer::AppendChild(UniquePtr<nsMenuObject> aChild,
+                             bool aUpdateNative)
+{
+    if (aUpdateNative) {
+        aChild->CreateNativeData();
+        MOZ_ALWAYS_TRUE(
+            dbusmenu_menuitem_child_append(GetNativeData(),
+                                           aChild->GetNativeData()));
+    }
+
+    mChildren.AppendElement(std::move(aChild));
+}
+
+bool
+nsMenuContainer::NeedsRebuild() const
+{
+    return false;
+}
+
+/* static */ nsIContent*
+nsMenuContainer::GetPreviousSupportedSibling(nsIContent *aContent)
+{
+    do {
+        aContent = aContent->GetPreviousSibling();
+    } while (aContent && !ContentIsSupported(aContent));
+
+    return aContent;
+}
--- /dev/null
+++ b/widget/gtk/nsMenuContainer.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsMenuContainer_h__
+#define __nsMenuContainer_h__
+
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+
+#include "nsMenuObject.h"
+
+class nsIContent;
+class nsNativeMenuDocListener;
+
+// Base class for containers (menus and menubars)
+class nsMenuContainer : public nsMenuObject
+{
+public:
+    typedef nsTArray<mozilla::UniquePtr<nsMenuObject> > ChildTArray;
+
+    // Determine if this container is being displayed on screen. Must be
+    // implemented by subclasses. Must return true if the container is
+    // in the fully open state, or false otherwise
+    virtual bool IsBeingDisplayed() const = 0;
+
+    // Determine if this container will be rebuilt the next time it opens.
+    // Returns false by default but can be overridden by subclasses
+    virtual bool NeedsRebuild() const;
+
+    // Return the first previous sibling that is of a type supported by the
+    // menu system
+    static nsIContent* GetPreviousSupportedSibling(nsIContent *aContent);
+
+    static const ChildTArray::index_type NoIndex;
+
+protected:
+    nsMenuContainer(nsMenuContainer *aParent, nsIContent *aContent);
+    nsMenuContainer(nsNativeMenuDocListener *aListener, nsIContent *aContent);
+
+    // Create a new child element for the specified content node
+    mozilla::UniquePtr<nsMenuObject> CreateChild(nsIContent *aContent);
+
+    // Return the index of the child for the specified content node
+    size_t IndexOf(nsIContent *aChild) const;
+
+    size_t ChildCount() const { return mChildren.Length(); }
+    nsMenuObject* ChildAt(size_t aIndex) const { return mChildren[aIndex].get(); }
+
+    void RemoveChildAt(size_t aIndex, bool aUpdateNative = true);
+
+    // Remove the child that owns the specified content node
+    void RemoveChild(nsIContent *aChild, bool aUpdateNative = true);
+
+    // Insert a new child after the child that owns the specified content node
+    void InsertChildAfter(mozilla::UniquePtr<nsMenuObject> aChild,
+                          nsIContent *aPrevSibling,
+                          bool aUpdateNative = true);
+
+    void AppendChild(mozilla::UniquePtr<nsMenuObject> aChild,
+                     bool aUpdateNative = true);
+
+private:
+    ChildTArray mChildren;
+};
+
+#endif /* __nsMenuContainer_h__ */
--- /dev/null
+++ b/widget/gtk/nsMenuItem.cpp
@@ -0,0 +1,766 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include "mozilla/dom/XULCommandEvent.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TextEvents.h"
+#include "nsContentUtils.h"
+#include "nsCRT.h"
+#include "nsGkAtoms.h"
+#include "nsGlobalWindowInner.h"
+#include "nsGtkUtils.h"
+#include "nsIContent.h"
+#include "nsIRunnable.h"
+#include "nsQueryObject.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+
+#include "nsMenu.h"
+#include "nsMenuBar.h"
+#include "nsMenuContainer.h"
+#include "nsNativeMenuDocListener.h"
+
+#include <gdk/gdk.h>
+#include <gdk/gdkkeysyms.h>
+#include <gdk/gdkkeysyms-compat.h>
+#include <gdk/gdkx.h>
+#include <gtk/gtk.h>
+
+#include "nsMenuItem.h"
+
+using namespace mozilla;
+
+struct KeyCodeData {
+    const char* str;
+    size_t strlength;
+    uint32_t keycode;
+};
+
+static struct KeyCodeData gKeyCodes[] = {
+#define NS_DEFINE_VK(aDOMKeyName, aDOMKeyCode) \
+  { #aDOMKeyName, sizeof(#aDOMKeyName) - 1, aDOMKeyCode },
+#include "mozilla/VirtualKeyCodeList.h"
+#undef NS_DEFINE_VK
+  { nullptr, 0, 0 }
+};
+
+struct KeyPair {
+    uint32_t DOMKeyCode;
+    guint GDKKeyval;
+};
+
+//
+// Netscape keycodes are defined in widget/public/nsGUIEvent.h
+// GTK keycodes are defined in <gdk/gdkkeysyms.h>
+//
+static const KeyPair gKeyPairs[] = {
+    { NS_VK_CANCEL,     GDK_Cancel },
+    { NS_VK_BACK,       GDK_BackSpace },
+    { NS_VK_TAB,        GDK_Tab },
+    { NS_VK_TAB,        GDK_ISO_Left_Tab },
+    { NS_VK_CLEAR,      GDK_Clear },
+    { NS_VK_RETURN,     GDK_Return },
+    { NS_VK_SHIFT,      GDK_Shift_L },
+    { NS_VK_SHIFT,      GDK_Shift_R },
+    { NS_VK_SHIFT,      GDK_Shift_Lock },
+    { NS_VK_CONTROL,    GDK_Control_L },
+    { NS_VK_CONTROL,    GDK_Control_R },
+    { NS_VK_ALT,        GDK_Alt_L },
+    { NS_VK_ALT,        GDK_Alt_R },
+    { NS_VK_META,       GDK_Meta_L },
+    { NS_VK_META,       GDK_Meta_R },
+
+    // Assume that Super or Hyper is always mapped to physical Win key.
+    { NS_VK_WIN,        GDK_Super_L },
+    { NS_VK_WIN,        GDK_Super_R },
+    { NS_VK_WIN,        GDK_Hyper_L },
+    { NS_VK_WIN,        GDK_Hyper_R },
+
+    // GTK's AltGraph key is similar to Mac's Option (Alt) key.  However,
+    // unfortunately, browsers on Mac are using NS_VK_ALT for it even though
+    // it's really different from Alt key on Windows.
+    // On the other hand, GTK's AltGrapsh keys are really different from
+    // Alt key.  However, there is no AltGrapsh key on Windows.  On Windows,
+    // both Ctrl and Alt keys are pressed internally when AltGr key is pressed.
+    // For some languages' users, AltGraph key is important, so, web
+    // applications on such locale may want to know AltGraph key press.
+    // Therefore, we should map AltGr keycode for them only on GTK.
+    { NS_VK_ALTGR,      GDK_ISO_Level3_Shift },
+    { NS_VK_ALTGR,      GDK_ISO_Level5_Shift },
+    // We assume that Mode_switch is always used for level3 shift.
+    { NS_VK_ALTGR,      GDK_Mode_switch },
+
+    { NS_VK_PAUSE,      GDK_Pause },
+    { NS_VK_CAPS_LOCK,  GDK_Caps_Lock },
+    { NS_VK_KANA,       GDK_Kana_Lock },
+    { NS_VK_KANA,       GDK_Kana_Shift },
+    { NS_VK_HANGUL,     GDK_Hangul },
+    // { NS_VK_JUNJA,      GDK_XXX },
+    // { NS_VK_FINAL,      GDK_XXX },
+    { NS_VK_HANJA,      GDK_Hangul_Hanja },
+    { NS_VK_KANJI,      GDK_Kanji },
+    { NS_VK_ESCAPE,     GDK_Escape },
+    { NS_VK_CONVERT,    GDK_Henkan },
+    { NS_VK_NONCONVERT, GDK_Muhenkan },
+    // { NS_VK_ACCEPT,     GDK_XXX },
+    // { NS_VK_MODECHANGE, GDK_XXX },
+    { NS_VK_SPACE,      GDK_space },
+    { NS_VK_PAGE_UP,    GDK_Page_Up },
+    { NS_VK_PAGE_DOWN,  GDK_Page_Down },
+    { NS_VK_END,        GDK_End },
+    { NS_VK_HOME,       GDK_Home },
+    { NS_VK_LEFT,       GDK_Left },
+    { NS_VK_UP,         GDK_Up },
+    { NS_VK_RIGHT,      GDK_Right },
+    { NS_VK_DOWN,       GDK_Down },
+    { NS_VK_SELECT,     GDK_Select },
+    { NS_VK_PRINT,      GDK_Print },
+    { NS_VK_EXECUTE,    GDK_Execute },
+    { NS_VK_PRINTSCREEN, GDK_Print },
+    { NS_VK_INSERT,     GDK_Insert },
+    { NS_VK_DELETE,     GDK_Delete },
+    { NS_VK_HELP,       GDK_Help },
+
+    // keypad keys
+    { NS_VK_LEFT,       GDK_KP_Left },
+    { NS_VK_RIGHT,      GDK_KP_Right },
+    { NS_VK_UP,         GDK_KP_Up },
+    { NS_VK_DOWN,       GDK_KP_Down },
+    { NS_VK_PAGE_UP,    GDK_KP_Page_Up },
+    // Not sure what these are
+    //{ NS_VK_,       GDK_KP_Prior },
+    //{ NS_VK_,        GDK_KP_Next },
+    { NS_VK_CLEAR,      GDK_KP_Begin }, // Num-unlocked 5
+    { NS_VK_PAGE_DOWN,  GDK_KP_Page_Down },
+    { NS_VK_HOME,       GDK_KP_Home },
+    { NS_VK_END,        GDK_KP_End },
+    { NS_VK_INSERT,     GDK_KP_Insert },
+    { NS_VK_DELETE,     GDK_KP_Delete },
+    { NS_VK_RETURN,     GDK_KP_Enter },
+
+    { NS_VK_NUM_LOCK,   GDK_Num_Lock },
+    { NS_VK_SCROLL_LOCK,GDK_Scroll_Lock },
+
+    // Function keys
+    { NS_VK_F1,         GDK_F1 },
+    { NS_VK_F2,         GDK_F2 },
+    { NS_VK_F3,         GDK_F3 },
+    { NS_VK_F4,         GDK_F4 },
+    { NS_VK_F5,         GDK_F5 },
+    { NS_VK_F6,         GDK_F6 },
+    { NS_VK_F7,         GDK_F7 },
+    { NS_VK_F8,         GDK_F8 },
+    { NS_VK_F9,         GDK_F9 },
+    { NS_VK_F10,        GDK_F10 },
+    { NS_VK_F11,        GDK_F11 },
+    { NS_VK_F12,        GDK_F12 },
+    { NS_VK_F13,        GDK_F13 },
+    { NS_VK_F14,        GDK_F14 },
+    { NS_VK_F15,        GDK_F15 },
+    { NS_VK_F16,        GDK_F16 },
+    { NS_VK_F17,        GDK_F17 },
+    { NS_VK_F18,        GDK_F18 },
+    { NS_VK_F19,        GDK_F19 },
+    { NS_VK_F20,        GDK_F20 },
+    { NS_VK_F21,        GDK_F21 },
+    { NS_VK_F22,        GDK_F22 },
+    { NS_VK_F23,        GDK_F23 },
+    { NS_VK_F24,        GDK_F24 },
+
+    // context menu key, keysym 0xff67, typically keycode 117 on 105-key (Microsoft)
+    // x86 keyboards, located between right 'Windows' key and right Ctrl key
+    { NS_VK_CONTEXT_MENU, GDK_Menu },
+    { NS_VK_SLEEP,      GDK_Sleep },
+
+    { NS_VK_ATTN,       GDK_3270_Attn },
+    { NS_VK_CRSEL,      GDK_3270_CursorSelect },
+    { NS_VK_EXSEL,      GDK_3270_ExSelect },
+    { NS_VK_EREOF,      GDK_3270_EraseEOF },
+    { NS_VK_PLAY,       GDK_3270_Play },
+    //{ NS_VK_ZOOM,       GDK_XXX },
+    { NS_VK_PA1,        GDK_3270_PA1 },
+};
+
+static guint
+ConvertGeckoKeyNameToGDKKeyval(nsAString& aKeyName)
+{
+    NS_ConvertUTF16toUTF8 keyName(aKeyName);
+    ToUpperCase(keyName); // We want case-insensitive comparison with data
+                          // stored as uppercase.
+
+    uint32_t keyCode = 0;
+
+    uint32_t keyNameLength = keyName.Length();
+    const char* keyNameStr = keyName.get();
+    for (uint16_t i = 0; i < ArrayLength(gKeyCodes); ++i) {
+        if (keyNameLength == gKeyCodes[i].strlength &&
+            !nsCRT::strcmp(gKeyCodes[i].str, keyNameStr)) {
+            keyCode = gKeyCodes[i].keycode;
+            break;
+        }
+    }
+
+    // First, try to handle alphanumeric input, not listed in nsKeycodes:
+    // most likely, more letters will be getting typed in than things in
+    // the key list, so we will look through these first.
+
+    if (keyCode >= NS_VK_A && keyCode <= NS_VK_Z) {
+        // gdk and DOM both use the ASCII codes for these keys.
+        return keyCode;
+    }
+
+    // numbers
+    if (keyCode >= NS_VK_0 && keyCode <= NS_VK_9) {
+        // gdk and DOM both use the ASCII codes for these keys.
+        return keyCode - NS_VK_0 + GDK_0;
+    }
+
+    switch (keyCode) {
+        // keys in numpad
+        case NS_VK_MULTIPLY:  return GDK_KP_Multiply;
+        case NS_VK_ADD:       return GDK_KP_Add;
+        case NS_VK_SEPARATOR: return GDK_KP_Separator;
+        case NS_VK_SUBTRACT:  return GDK_KP_Subtract;
+        case NS_VK_DECIMAL:   return GDK_KP_Decimal;
+        case NS_VK_DIVIDE:    return GDK_KP_Divide;
+        case NS_VK_NUMPAD0:   return GDK_KP_0;
+        case NS_VK_NUMPAD1:   return GDK_KP_1;
+        case NS_VK_NUMPAD2:   return GDK_KP_2;
+        case NS_VK_NUMPAD3:   return GDK_KP_3;
+        case NS_VK_NUMPAD4:   return GDK_KP_4;
+        case NS_VK_NUMPAD5:   return GDK_KP_5;
+        case NS_VK_NUMPAD6:   return GDK_KP_6;
+        case NS_VK_NUMPAD7:   return GDK_KP_7;
+        case NS_VK_NUMPAD8:   return GDK_KP_8;
+        case NS_VK_NUMPAD9:   return GDK_KP_9;
+        // other prinable keys
+        case NS_VK_SPACE:               return GDK_space;
+        case NS_VK_COLON:               return GDK_colon;
+        case NS_VK_SEMICOLON:           return GDK_semicolon;
+        case NS_VK_LESS_THAN:           return GDK_less;
+        case NS_VK_EQUALS:              return GDK_equal;
+        case NS_VK_GREATER_THAN:        return GDK_greater;
+        case NS_VK_QUESTION_MARK:       return GDK_question;
+        case NS_VK_AT:                  return GDK_at;
+        case NS_VK_CIRCUMFLEX:          return GDK_asciicircum;
+        case NS_VK_EXCLAMATION:         return GDK_exclam;
+        case NS_VK_DOUBLE_QUOTE:        return GDK_quotedbl;
+        case NS_VK_HASH:                return GDK_numbersign;
+        case NS_VK_DOLLAR:              return GDK_dollar;
+        case NS_VK_PERCENT:             return GDK_percent;
+        case NS_VK_AMPERSAND:           return GDK_ampersand;
+        case NS_VK_UNDERSCORE:          return GDK_underscore;
+        case NS_VK_OPEN_PAREN:          return GDK_parenleft;
+        case NS_VK_CLOSE_PAREN:         return GDK_parenright;
+        case NS_VK_ASTERISK:            return GDK_asterisk;
+        case NS_VK_PLUS:                return GDK_plus;
+        case NS_VK_PIPE:                return GDK_bar;
+        case NS_VK_HYPHEN_MINUS:        return GDK_minus;
+        case NS_VK_OPEN_CURLY_BRACKET:  return GDK_braceleft;
+        case NS_VK_CLOSE_CURLY_BRACKET: return GDK_braceright;
+        case NS_VK_TILDE:               return GDK_asciitilde;
+        case NS_VK_COMMA:               return GDK_comma;
+        case NS_VK_PERIOD:              return GDK_period;
+        case NS_VK_SLASH:               return GDK_slash;
+        case NS_VK_BACK_QUOTE:          return GDK_grave;
+        case NS_VK_OPEN_BRACKET:        return GDK_bracketleft;
+        case NS_VK_BACK_SLASH:          return GDK_backslash;
+        case NS_VK_CLOSE_BRACKET:       return GDK_bracketright;
+        case NS_VK_QUOTE:               return GDK_apostrophe;
+    }
+
+    // misc other things
+    for (uint32_t i = 0; i < ArrayLength(gKeyPairs); ++i) {
+        if (gKeyPairs[i].DOMKeyCode == keyCode) {
+            return gKeyPairs[i].GDKKeyval;
+        }
+    }
+
+    return 0;
+}
+
+class nsMenuItemUncheckSiblingsRunnable final : public Runnable
+{
+public:
+    NS_IMETHODIMP Run()
+    {
+        if (mMenuItem) {
+            static_cast<nsMenuItem *>(mMenuItem.get())->UncheckSiblings();
+        }
+        return NS_OK;
+    }
+
+    nsMenuItemUncheckSiblingsRunnable(nsMenuItem *aMenuItem) :
+        Runnable("nsMenuItemUncheckSiblingsRunnable"),
+        mMenuItem(aMenuItem) { };
+
+private:
+    nsWeakMenuObject mMenuItem;
+};
+
+bool
+nsMenuItem::IsCheckboxOrRadioItem() const
+{
+    return mType == eMenuItemType_Radio ||
+           mType == eMenuItemType_CheckBox;
+}
+
+/* static */ void
+nsMenuItem::item_activated_cb(DbusmenuMenuitem *menuitem,
+                              guint timestamp,
+                              gpointer user_data)
+{
+    nsMenuItem *item = static_cast<nsMenuItem *>(user_data);
+    item->Activate(timestamp);
+}
+
+void
+nsMenuItem::Activate(uint32_t aTimestamp)
+{
+    GdkWindow *window = gtk_widget_get_window(MenuBar()->TopLevelWindow());
+    gdk_x11_window_set_user_time(
+        window, std::min(aTimestamp, gdk_x11_get_server_time(window)));
+
+    // We do this to avoid mutating our view of the menu until
+    // after we have finished
+    nsNativeMenuDocListener::BlockUpdatesScope updatesBlocker;
+
+    if (!ContentNode()->AsElement()->AttrValueIs(kNameSpaceID_None,
+                                                 nsGkAtoms::autocheck,
+                                                 nsGkAtoms::_false,
+                                                 eCaseMatters) &&
+        (mType == eMenuItemType_CheckBox ||
+         (mType == eMenuItemType_Radio && !mIsChecked))) {
+        ContentNode()->AsElement()->SetAttr(kNameSpaceID_None,
+                                            nsGkAtoms::checked,
+                                            mIsChecked ?
+                                                u"false"_ns
+                                                : u"true"_ns,
+                                            true);
+    }
+
+    dom::Document *doc = ContentNode()->OwnerDoc();
+    ErrorResult rv;
+    RefPtr<dom::Event> event =
+        doc->CreateEvent(u"xulcommandevent"_ns,
+                         dom::CallerType::System, rv);
+    if (!rv.Failed()) {
+        RefPtr<dom::XULCommandEvent> command = event->AsXULCommandEvent();
+        if (command) {
+            command->InitCommandEvent(u"command"_ns, true, true,
+                                      nsGlobalWindowInner::Cast(doc->GetInnerWindow()),
+                                      0, false, false, false, false, 0, nullptr, 0, rv);
+            if (!rv.Failed()) {
+                event->SetTrusted(true);
+                ContentNode()->DispatchEvent(*event, rv);
+                if (rv.Failed()) {
+                    NS_WARNING("Failed to dispatch event");
+                    rv.SuppressException();
+                }
+            } else {
+                NS_WARNING("Failed to initialize command event");
+                rv.SuppressException();
+            }
+        }
+    } else {
+        NS_WARNING("CreateEvent failed");
+        rv.SuppressException();
+    }
+
+    // This kinda sucks, but Unity doesn't send a closed event
+    // after activating a menuitem
+    nsMenuObject *ancestor = Parent();
+    while (ancestor && ancestor->Type() == eType_Menu) {
+        static_cast<nsMenu *>(ancestor)->OnClose();
+        ancestor = ancestor->Parent();
+    }
+}
+
+void
+nsMenuItem::CopyAttrFromNodeIfExists(nsIContent *aContent, nsAtom *aAttribute)
+{
+    nsAutoString value;
+    if (aContent->AsElement()->GetAttr(kNameSpaceID_None, aAttribute, value)) {
+        ContentNode()->AsElement()->SetAttr(kNameSpaceID_None, aAttribute,
+                                            value, true);
+    }
+}
+
+void
+nsMenuItem::UpdateState()
+{
+    if (!IsCheckboxOrRadioItem()) {
+        return;
+    }
+
+    mIsChecked = ContentNode()->AsElement()->AttrValueIs(kNameSpaceID_None,
+                                                         nsGkAtoms::checked,
+                                                         nsGkAtoms::_true,
+                                                         eCaseMatters);
+    dbusmenu_menuitem_property_set_int(GetNativeData(),
+                                       DBUSMENU_MENUITEM_PROP_TOGGLE_STATE,
+                                       mIsChecked ?
+                                         DBUSMENU_MENUITEM_TOGGLE_STATE_CHECKED :
+                                         DBUSMENU_MENUITEM_TOGGLE_STATE_UNCHECKED);
+}
+
+void
+nsMenuItem::UpdateTypeAndState()
+{
+    static mozilla::dom::Element::AttrValuesArray attrs[] =
+        { nsGkAtoms::checkbox, nsGkAtoms::radio, nullptr };
+    int32_t type = ContentNode()->AsElement()->FindAttrValueIn(kNameSpaceID_None,
+                                                               nsGkAtoms::type,
+                                                               attrs, eCaseMatters);
+
+    if (type >= 0 && type < 2) {
+        if (type == 0) {
+            dbusmenu_menuitem_property_set(GetNativeData(),
+                                           DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE,
+                                           DBUSMENU_MENUITEM_TOGGLE_CHECK);
+            mType = eMenuItemType_CheckBox;
+        } else if (type == 1) {
+            dbusmenu_menuitem_property_set(GetNativeData(),
+                                           DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE,
+                                           DBUSMENU_MENUITEM_TOGGLE_RADIO);
+            mType = eMenuItemType_Radio;
+        }
+
+        UpdateState();
+    } else {
+        dbusmenu_menuitem_property_remove(GetNativeData(),
+                                          DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE);
+        dbusmenu_menuitem_property_remove(GetNativeData(),
+                                          DBUSMENU_MENUITEM_PROP_TOGGLE_STATE);
+        mType = eMenuItemType_Normal;
+    }
+}
+
+void
+nsMenuItem::UpdateAccel()
+{
+    dom::Document *doc = ContentNode()->GetUncomposedDoc();
+    if (doc) {
+        nsCOMPtr<nsIContent> oldKeyContent;
+        oldKeyContent.swap(mKeyContent);
+
+        nsAutoString key;
+        ContentNode()->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::key,
+                                            key);
+        if (!key.IsEmpty()) {
+            mKeyContent = doc->GetElementById(key);
+        }
+
+        if (mKeyContent != oldKeyContent) {
+            if (oldKeyContent) {
+                DocListener()->UnregisterForContentChanges(oldKeyContent);
+            }
+            if (mKeyContent) {
+                DocListener()->RegisterForContentChanges(mKeyContent, this);
+            }
+        }
+    }
+
+    if (!mKeyContent) {
+        dbusmenu_menuitem_property_remove(GetNativeData(),
+                                          DBUSMENU_MENUITEM_PROP_SHORTCUT);
+        return;
+    }
+
+    nsAutoString modifiers;
+    mKeyContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers,
+                                      modifiers);
+
+    uint32_t modifier = 0;
+
+    if (!modifiers.IsEmpty()) {
+        char* str = ToNewUTF8String(modifiers);
+        char *token = strtok(str, ", \t");
+        while(token) {
+            if (nsCRT::strcmp(token, "shift") == 0) {
+                modifier |= GDK_SHIFT_MASK;
+            } else if (nsCRT::strcmp(token, "alt") == 0) {
+                modifier |= GDK_MOD1_MASK;
+            } else if (nsCRT::strcmp(token, "meta") == 0) {
+                modifier |= GDK_META_MASK;
+            } else if (nsCRT::strcmp(token, "control") == 0) {
+                modifier |= GDK_CONTROL_MASK;
+            } else if (nsCRT::strcmp(token, "accel") == 0) {
+                int32_t accel = Preferences::GetInt("ui.key.accelKey");
+                if (accel == dom::KeyboardEvent_Binding::DOM_VK_META) {
+                    modifier |= GDK_META_MASK;
+                } else if (accel == dom::KeyboardEvent_Binding::DOM_VK_ALT) {
+                    modifier |= GDK_MOD1_MASK;
+                } else {
+                    modifier |= GDK_CONTROL_MASK;
+                }
+            }
+
+            token = strtok(nullptr, ", \t");
+        }
+
+        free(str);
+    }
+
+    nsAutoString keyStr;
+    mKeyContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::key,
+                                      keyStr);
+
+    guint key = 0;
+    if (!keyStr.IsEmpty()) {
+        key = gdk_unicode_to_keyval(*keyStr.BeginReading());
+    }
+
+    if (key == 0) {
+        mKeyContent->AsElement()->GetAttr(kNameSpaceID_None,
+                                          nsGkAtoms::keycode, keyStr);
+        if (!keyStr.IsEmpty()) {
+            key = ConvertGeckoKeyNameToGDKKeyval(keyStr);
+        }
+    }
+
+    if (key == 0) {
+        key = GDK_VoidSymbol;
+    }
+
+    if (key != GDK_VoidSymbol) {
+        dbusmenu_menuitem_property_set_shortcut(GetNativeData(), key,
+                                                static_cast<GdkModifierType>(modifier));
+    } else {
+        dbusmenu_menuitem_property_remove(GetNativeData(),
+                                          DBUSMENU_MENUITEM_PROP_SHORTCUT);
+    }
+}
+
+nsMenuBar*
+nsMenuItem::MenuBar()
+{
+    nsMenuObject *tmp = this;
+    while (tmp->Parent()) {
+        tmp = tmp->Parent();
+    }
+
+    MOZ_ASSERT(tmp->Type() == eType_MenuBar, "The top-level should be a menubar");
+
+    return static_cast<nsMenuBar *>(tmp);
+}
+
+void
+nsMenuItem::UncheckSiblings()
+{
+    if (!ContentNode()->AsElement()->AttrValueIs(kNameSpaceID_None,
+                                                 nsGkAtoms::type,
+                                                 nsGkAtoms::radio,
+                                                 eCaseMatters)) {
+        // If we're not a radio button, we don't care
+        return;
+    }
+
+    nsAutoString name;
+    ContentNode()->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::name,
+                                        name);
+
+    nsIContent *parent = ContentNode()->GetParent();
+    if (!parent) {
+        return;
+    }
+
+    uint32_t count = parent->GetChildCount();
+    for (uint32_t i = 0; i < count; ++i) {
+        nsIContent *sibling = parent->GetChildAt_Deprecated(i);
+
+        if (sibling->IsComment()) {
+            continue;
+        }
+
+        nsAutoString otherName;
+        sibling->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::name,
+                                      otherName);
+
+        if (sibling != ContentNode() && otherName == name &&
+            sibling->AsElement()->AttrValueIs(kNameSpaceID_None,
+                                              nsGkAtoms::type,
+                                              nsGkAtoms::radio,
+                                              eCaseMatters)) {
+            sibling->AsElement()->UnsetAttr(kNameSpaceID_None,
+                                            nsGkAtoms::checked, true);
+        }
+    }
+}
+
+void
+nsMenuItem::InitializeNativeData()
+{
+    g_signal_connect(G_OBJECT(GetNativeData()),
+                     DBUSMENU_MENUITEM_SIGNAL_ITEM_ACTIVATED,
+                     G_CALLBACK(item_activated_cb), this);
+    mNeedsUpdate = true;
+}
+
+void
+nsMenuItem::UpdateContentAttributes()
+{
+    dom::Document *doc = ContentNode()->GetUncomposedDoc();
+    if (!doc) {
+        return;
+    }
+
+    nsAutoString command;
+    ContentNode()->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::command,
+                                        command);
+    if (command.IsEmpty()) {
+        return;
+    }
+
+    nsCOMPtr<nsIContent> commandContent = doc->GetElementById(command);
+    if (!commandContent) {
+        return;
+    }
+
+    if (commandContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+                                                 nsGkAtoms::disabled,
+                                                 nsGkAtoms::_true,
+                                                 eCaseMatters)) {
+        ContentNode()->AsElement()->SetAttr(kNameSpaceID_None,
+                                            nsGkAtoms::disabled,
+                                            u"true"_ns, true);
+    } else {
+        ContentNode()->AsElement()->UnsetAttr(kNameSpaceID_None,
+                                              nsGkAtoms::disabled, true);
+    }
+
+    CopyAttrFromNodeIfExists(commandContent, nsGkAtoms::checked);
+    CopyAttrFromNodeIfExists(commandContent, nsGkAtoms::accesskey);
+    CopyAttrFromNodeIfExists(commandContent, nsGkAtoms::label);
+    CopyAttrFromNodeIfExists(commandContent, nsGkAtoms::hidden);
+}
+
+void
+nsMenuItem::Update(const ComputedStyle *aComputedStyle)
+{
+    if (mNeedsUpdate) {
+        mNeedsUpdate = false;
+
+        UpdateTypeAndState();
+        UpdateAccel();
+        UpdateLabel();
+        UpdateSensitivity();
+    }
+
+    UpdateVisibility(aComputedStyle);
+    UpdateIcon(aComputedStyle);
+}
+
+bool
+nsMenuItem::IsCompatibleWithNativeData(DbusmenuMenuitem *aNativeData) const
+{
+    return nsCRT::strcmp(dbusmenu_menuitem_property_get(aNativeData,
+                                                        DBUSMENU_MENUITEM_PROP_TYPE),
+                         "separator") != 0;
+}
+
+nsMenuObject::PropertyFlags
+nsMenuItem::SupportedProperties() const
+{
+    return static_cast<nsMenuObject::PropertyFlags>(
+        nsMenuObject::ePropLabel |
+        nsMenuObject::ePropEnabled |
+        nsMenuObject::ePropVisible |
+        nsMenuObject::ePropIconData |
+        nsMenuObject::ePropShortcut |
+        nsMenuObject::ePropToggleType |
+        nsMenuObject::ePropToggleState
+    );
+}
+
+void
+nsMenuItem::OnAttributeChanged(nsIContent *aContent, nsAtom *aAttribute)
+{
+    MOZ_ASSERT(aContent == ContentNode() || aContent == mKeyContent,
+               "Received an event that wasn't meant for us!");
+
+    if (aContent == ContentNode() && aAttribute == nsGkAtoms::checked &&
+        aContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+                                           nsGkAtoms::checked,
+                                           nsGkAtoms::_true, eCaseMatters)) {
+        nsContentUtils::AddScriptRunner(
+            new nsMenuItemUncheckSiblingsRunnable(this));
+    }
+
+    if (mNeedsUpdate) {
+        return;
+    }
+
+    if (!Parent()->IsBeingDisplayed()) {
+        mNeedsUpdate = true;
+        return;
+    }
+
+    if (aContent == ContentNode()) {
+        if (aAttribute == nsGkAtoms::key) {
+            UpdateAccel();
+        } else if (aAttribute == nsGkAtoms::label ||
+                   aAttribute == nsGkAtoms::accesskey ||
+                   aAttribute == nsGkAtoms::crop) {
+            UpdateLabel();
+        } else if (aAttribute == nsGkAtoms::disabled) {
+            UpdateSensitivity();
+        } else if (aAttribute == nsGkAtoms::type) {
+            UpdateTypeAndState();
+        } else if (aAttribute == nsGkAtoms::checked) {
+            UpdateState();
+        } else if (aAttribute == nsGkAtoms::hidden ||
+                   aAttribute == nsGkAtoms::collapsed) {
+            RefPtr<const ComputedStyle> style = GetComputedStyle();
+            UpdateVisibility(style);
+        } else if (aAttribute == nsGkAtoms::image) {
+            RefPtr<const ComputedStyle> style = GetComputedStyle();
+            UpdateIcon(style);
+        }
+    } else if (aContent == mKeyContent &&
+               (aAttribute == nsGkAtoms::key ||
+                aAttribute == nsGkAtoms::keycode ||
+                aAttribute == nsGkAtoms::modifiers)) {
+        UpdateAccel();
+    }
+}
+
+nsMenuItem::nsMenuItem(nsMenuContainer *aParent, nsIContent *aContent) :
+    nsMenuObject(aParent, aContent),
+    mType(eMenuItemType_Normal),
+    mIsChecked(false),
+    mNeedsUpdate(false)
+{
+    MOZ_COUNT_CTOR(nsMenuItem);
+}
+
+nsMenuItem::~nsMenuItem()
+{
+    if (DocListener() && mKeyContent) {
+        DocListener()->UnregisterForContentChanges(mKeyContent);
+    }
+
+    if (GetNativeData()) {
+        g_signal_handlers_disconnect_by_func(GetNativeData(),
+                                             FuncToGpointer(item_activated_cb),
+                                             this);
+    }
+
+    MOZ_COUNT_DTOR(nsMenuItem);
+}
+
+nsMenuObject::EType
+nsMenuItem::Type() const
+{
+    return eType_MenuItem;
+}
--- /dev/null
+++ b/widget/gtk/nsMenuItem.h
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsMenuItem_h__
+#define __nsMenuItem_h__
+
+#include "mozilla/Attributes.h"
+#include "nsCOMPtr.h"
+
+#include "nsDbusmenu.h"
+#include "nsMenuObject.h"
+
+#include <glib.h>
+
+class nsAtom;
+class nsIContent;
+class nsMenuBar;
+class nsMenuContainer;
+
+/*
+ * This class represents 3 main classes of menuitems: labels, checkboxes and
+ * radio buttons (with/without an icon)
+ */
+class nsMenuItem final : public nsMenuObject
+{
+public:
+    nsMenuItem(nsMenuContainer *aParent, nsIContent *aContent);
+    ~nsMenuItem() override;
+
+    nsMenuObject::EType Type() const override;
+
+private:
+    friend class nsMenuItemUncheckSiblingsRunnable;
+
+    enum {
+        eMenuItemFlag_ToggleState = (1 << 0)
+    };
+
+    enum EMenuItemType {
+        eMenuItemType_Normal,
+        eMenuItemType_Radio,
+        eMenuItemType_CheckBox
+    };
+
+    bool IsCheckboxOrRadioItem() const;
+
+    static void item_activated_cb(DbusmenuMenuitem *menuitem,
+                                  guint timestamp,
+                                  gpointer user_data);
+    void Activate(uint32_t aTimestamp);
+
+    void CopyAttrFromNodeIfExists(nsIContent *aContent, nsAtom *aAtom);
+    void UpdateState();
+    void UpdateTypeAndState();
+    void UpdateAccel();
+    nsMenuBar* MenuBar();
+    void UncheckSiblings();
+
+    void InitializeNativeData() override;
+    void UpdateContentAttributes() override;
+    void Update(const mozilla::ComputedStyle *aComputedStyle) override;
+    bool IsCompatibleWithNativeData(DbusmenuMenuitem *aNativeData) const override;
+    nsMenuObject::PropertyFlags SupportedProperties() const override;
+
+    void OnAttributeChanged(nsIContent *aContent, nsAtom *aAttribute) override;
+
+    EMenuItemType mType;
+
+    bool mIsChecked;
+
+    bool mNeedsUpdate;
+
+    nsCOMPtr<nsIContent> mKeyContent;
+};
+
+#endif /* __nsMenuItem_h__ */
--- /dev/null
+++ b/widget/gtk/nsMenuObject.cpp
@@ -0,0 +1,653 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#include "ImageOps.h"
+#include "imgIContainer.h"
+#include "imgINotificationObserver.h"
+#include "imgLoader.h"
+#include "imgRequestProxy.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/PresShellInlines.h"
+#include "mozilla/GRefPtr.h"
+#include "nsAttrValue.h"
+#include "nsComputedDOMStyle.h"
+#include "nsContentUtils.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsIContentPolicy.h"
+#include "nsILoadGroup.h"
+#include "nsImageToPixbuf.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsPresContext.h"
+#include "nsRect.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsStyleConsts.h"
+#include "nsStyleStruct.h"
+#include "nsUnicharUtils.h"
+
+#include "nsMenuContainer.h"
+#include "nsNativeMenuDocListener.h"
+
+#include <gdk/gdk.h>
+#include <glib-object.h>
+#include <pango/pango.h>
+
+#include "nsMenuObject.h"
+
+// X11's None clashes with StyleDisplay::None
+#include "X11UndefineNone.h"
+
+#undef None
+
+using namespace mozilla;
+using mozilla::image::ImageOps;
+
+#define MAX_WIDTH 350000
+
+const char *gPropertyStrings[] = {
+#define DBUSMENU_PROPERTY(e, s, b) s,
+    DBUSMENU_PROPERTIES
+#undef DBUSMENU_PROPERTY
+    nullptr
+};
+
+nsWeakMenuObject* nsWeakMenuObject::sHead;
+PangoLayout* gPangoLayout = nullptr;
+
+class nsMenuObjectIconLoader final : public imgINotificationObserver
+{
+public:
+    NS_DECL_ISUPPORTS
+    NS_DECL_IMGINOTIFICATIONOBSERVER
+
+    nsMenuObjectIconLoader(nsMenuObject *aOwner) : mOwner(aOwner) { };
+
+    void LoadIcon(const ComputedStyle *aComputedStyle);
+    void Destroy();
+
+private:
+    ~nsMenuObjectIconLoader() { };
+
+    nsMenuObject *mOwner;
+    RefPtr<imgRequestProxy> mImageRequest;
+    nsCOMPtr<nsIURI> mURI;
+};
+
+NS_IMPL_ISUPPORTS(nsMenuObjectIconLoader, imgINotificationObserver)
+
+void
+nsMenuObjectIconLoader::Notify(imgIRequest *aProxy,
+                               int32_t aType, const nsIntRect *aRect)
+{
+    if (!mOwner) {
+        return;
+    }
+
+    if (aProxy != mImageRequest) {
+        return;
+    }
+
+    if (aType == imgINotificationObserver::LOAD_COMPLETE) {
+        uint32_t status = imgIRequest::STATUS_ERROR;
+        if (NS_FAILED(mImageRequest->GetImageStatus(&status)) ||
+            (status & imgIRequest::STATUS_ERROR)) {
+            mImageRequest->Cancel(NS_BINDING_ABORTED);
+            mImageRequest = nullptr;
+            return;
+        }
+
+        nsCOMPtr<imgIContainer> image;
+        mImageRequest->GetImage(getter_AddRefs(image));
+        MOZ_ASSERT(image);
+
+        // Ask the image to decode at its intrinsic size.
+        int32_t width = 0, height = 0;
+        image->GetWidth(&width);
+        image->GetHeight(&height);
+        image->RequestDecodeForSize(nsIntSize(width, height), imgIContainer::FLAG_NONE);
+        return;
+    }
+
+    if (aType == imgINotificationObserver::DECODE_COMPLETE) {
+        mImageRequest->Cancel(NS_BINDING_ABORTED);
+        mImageRequest = nullptr;
+        return;
+    }
+
+    if (aType != imgINotificationObserver::FRAME_COMPLETE) {
+        return;
+    }
+
+    nsCOMPtr<imgIContainer> img;
+    mImageRequest->GetImage(getter_AddRefs(img));
+    if (!img) {
+        return;
+    }
+
+    int32_t width, height;
+    img->GetWidth(&width);
+    img->GetHeight(&height);
+
+    if (width <= 0 || height <= 0) {
+        mOwner->ClearIcon();
+        return;
+    }
+
+    if (width > 100 || height > 100) {
+        // The icon data needs to go across DBus. Make sure the icon
+        // data isn't too large, else our connection gets terminated and
+        // GDbus helpfully aborts the application. Thank you :)
+        NS_WARNING("Icon data too large");
+        mOwner->ClearIcon();
+        return;
+    }
+
+    RefPtr<GdkPixbuf> pixbuf = nsImageToPixbuf::ImageToPixbuf(img);
+    if (pixbuf) {
+        dbusmenu_menuitem_property_set_image(mOwner->GetNativeData(),
+                                             DBUSMENU_MENUITEM_PROP_ICON_DATA,
+                                             pixbuf);
+    }
+
+    return;
+}
+
+void
+nsMenuObjectIconLoader::LoadIcon(const ComputedStyle *aComputedStyle)
+{
+    dom::Document *doc = mOwner->ContentNode()->OwnerDoc();
+
+    nsCOMPtr<nsIURI> uri;
+    imgRequestProxy *imageRequest = nullptr;
+
+    nsAutoString uriString;
+    if (mOwner->ContentNode()->AsElement()->GetAttr(kNameSpaceID_None,
+                                                    nsGkAtoms::image,
+                                                    uriString)) {
+        NS_NewURI(getter_AddRefs(uri), uriString);
+    } else {
+        PresShell *shell = doc->GetPresShell();
+        if (!shell) {
+            return;
+        }
+
+        nsPresContext *pc = shell->GetPresContext();
+        if (!pc || !aComputedStyle) {
+            return;
+        }
+
+        const nsStyleList *list = aComputedStyle->StyleList();
+        imageRequest = list->mListStyleImage.GetImageRequest();
+        if (imageRequest) {
+            imageRequest->GetURI(getter_AddRefs(uri));
+        }
+    }
+
+    if (!uri) {
+        mOwner->ClearIcon();
+        mURI = nullptr;
+
+        if (mImageRequest) {
+            mImageRequest->Cancel(NS_BINDING_ABORTED);
+            mImageRequest = nullptr;
+        }
+
+        return;
+    }
+
+    bool same;
+    if (mURI && NS_SUCCEEDED(mURI->Equals(uri, &same)) && same &&
+        !imageRequest) {
+        return;
+    }
+
+    if (mImageRequest) {
+        mImageRequest->Cancel(NS_BINDING_ABORTED);
+        mImageRequest = nullptr;
+    }
+
+    mURI = uri;
+
+    if (imageRequest) {
+        imageRequest->Clone(this, nullptr, getter_AddRefs(mImageRequest));
+    } else {
+        nsCOMPtr<nsILoadGroup> loadGroup = doc->GetDocumentLoadGroup();
+        RefPtr<imgLoader> loader =
+            nsContentUtils::GetImgLoaderForDocument(doc);
+        if (!loader || !loadGroup) {
+            NS_WARNING("Failed to get loader or load group for image load");
+            return;
+        }
+
+        loader->LoadImage(uri, nullptr, nullptr,
+                          nullptr, 0, loadGroup, this, nullptr, nullptr,
+                          nsIRequest::LOAD_NORMAL, nullptr,
+                          nsIContentPolicy::TYPE_IMAGE, EmptyString(),
+                          false, false, 0, getter_AddRefs(mImageRequest));
+    }
+}
+
+void
+nsMenuObjectIconLoader::Destroy()
+{
+    if (mImageRequest) {
+        mImageRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
+        mImageRequest = nullptr;
+    }
+
+    mOwner = nullptr;
+}
+
+static int
+CalculateTextWidth(const nsAString& aText)
+{
+    if (!gPangoLayout) {
+        PangoFontMap *fontmap = pango_cairo_font_map_get_default();
+        PangoContext *ctx = pango_font_map_create_context(fontmap);
+        gPangoLayout = pango_layout_new(ctx);
+        g_object_unref(ctx);
+    }
+
+    pango_layout_set_text(gPangoLayout, NS_ConvertUTF16toUTF8(aText).get(), -1);
+
+    int width, dummy;
+    pango_layout_get_size(gPangoLayout, &width, &dummy);
+
+    return width;
+}
+
+static const nsDependentString
+GetEllipsis()
+{
+    static char16_t sBuf[4] = { 0, 0, 0, 0 };
+    if (!sBuf[0]) {
+        nsString ellipsis;
+        Preferences::GetLocalizedString("intl.ellipsis", ellipsis);
+        if (!ellipsis.IsEmpty()) {
+            uint32_t l = ellipsis.Length();
+            const nsString::char_type *c = ellipsis.BeginReading();
+            uint32_t i = 0;
+            while (i < 3 && i < l) {
+                sBuf[i++] = *(c++);
+            }
+        } else {
+            sBuf[0] = '.';
+            sBuf[1] = '.';
+            sBuf[2] = '.';
+        }
+    }
+
+    return nsDependentString(sBuf);
+}
+
+static int
+GetEllipsisWidth()
+{
+    static int sEllipsisWidth = -1;
+
+    if (sEllipsisWidth == -1) {
+        sEllipsisWidth = CalculateTextWidth(GetEllipsis());
+    }
+
+    return sEllipsisWidth;
+}
+
+nsMenuObject::nsMenuObject(nsMenuContainer *aParent, nsIContent *aContent) :
+    mContent(aContent),
+    mListener(aParent->DocListener()),
+    mParent(aParent),
+    mNativeData(nullptr)
+{
+    MOZ_ASSERT(mContent);
+    MOZ_ASSERT(mListener);
+    MOZ_ASSERT(mParent);
+}
+
+nsMenuObject::nsMenuObject(nsNativeMenuDocListener *aListener,
+                           nsIContent *aContent) :
+    mContent(aContent),
+    mListener(aListener),
+    mParent(nullptr),
+    mNativeData(nullptr)
+{
+    MOZ_ASSERT(mContent);
+    MOZ_ASSERT(mListener);
+}
+
+void
+nsMenuObject::UpdateLabel()
+{
+    // Gecko stores the label and access key in separate attributes
+    // so we need to convert label="Foo_Bar"/accesskey="F" in to
+    // label="_Foo__Bar" for dbusmenu
+
+    nsAutoString label;
+    mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, label);
+
+    nsAutoString accesskey;
+    mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
+                                   accesskey);
+
+    const nsAutoString::char_type *akey = accesskey.BeginReading();
+    char16_t keyLower = ToLowerCase(*akey);
+    char16_t keyUpper = ToUpperCase(*akey);
+
+    const nsAutoString::char_type *iter = label.BeginReading();
+    const nsAutoString::char_type *end = label.EndReading();
+    uint32_t length = label.Length();
+    uint32_t pos = 0;
+    bool foundAccessKey = false;
+
+    while (iter != end) {
+        if (*iter != char16_t('_')) {
+            if ((*iter != keyLower && *iter != keyUpper) || foundAccessKey) {
+                ++iter;
+                ++pos;
+                continue;
+            }
+            foundAccessKey = true;
+        }
+
+        label.SetLength(++length);
+
+        iter = label.BeginReading() + pos;
+        end = label.EndReading();
+        nsAutoString::char_type *cur = label.BeginWriting() + pos;
+
+        memmove(cur + 1, cur, (length - 1 - pos) * sizeof(nsAutoString::char_type));
+        *cur = nsAutoString::char_type('_');
+
+        iter += 2;
+        pos += 2;
+    }
+
+    if (CalculateTextWidth(label) <= MAX_WIDTH) {
+        dbusmenu_menuitem_property_set(mNativeData,
+                                       DBUSMENU_MENUITEM_PROP_LABEL,
+                                       NS_ConvertUTF16toUTF8(label).get());
+        return;
+    }
+
+    // This sucks.
+    // This should be done at the point where the menu is drawn (hello Unity),
+    // but unfortunately it doesn't do that and will happily fill your entire
+    // screen width with a menu if you have a bookmark with a really long title.
+    // This leaves us with no other option but to ellipsize here, with no proper
+    // knowledge of Unity's render path, font size etc. This is better than nothing
+    nsAutoString truncated;
+    int target = MAX_WIDTH - GetEllipsisWidth();
+    length = label.Length();
+
+    static mozilla::dom::Element::AttrValuesArray strings[] = {
+        nsGkAtoms::left, nsGkAtoms::start,
+        nsGkAtoms::center, nsGkAtoms::right,
+        nsGkAtoms::end, nullptr
+    };
+
+    int32_t type = mContent->AsElement()->FindAttrValueIn(kNameSpaceID_None,
+                                                          nsGkAtoms::crop,
+                                                          strings, eCaseMatters);
+
+    switch (type) {
+        case 0:
+        case 1:
+            // FIXME: Implement left cropping
+        case 2:
+            // FIXME: Implement center cropping
+        case 3:
+        case 4:
+        default:
+            for (uint32_t i = 0; i < length; i++) {
+                truncated.Append(label.CharAt(i));
+                if (CalculateTextWidth(truncated) > target) {
+                    break;
+                }
+            }
+
+            truncated.Append(GetEllipsis());
+    }
+
+    dbusmenu_menuitem_property_set(mNativeData,
+                                   DBUSMENU_MENUITEM_PROP_LABEL,
+                                   NS_ConvertUTF16toUTF8(truncated).get());
+}
+
+void
+nsMenuObject::UpdateVisibility(const ComputedStyle *aComputedStyle)
+{
+    bool vis = true;
+
+    if (aComputedStyle &&
+        (aComputedStyle->StyleDisplay()->mDisplay == StyleDisplay::None ||
+         aComputedStyle->StyleVisibility()->mVisible ==
+            StyleVisibility::Collapse)) {
+        vis = false;
+    }
+
+    dbusmenu_menuitem_property_set_bool(mNativeData,
+                                        DBUSMENU_MENUITEM_PROP_VISIBLE,
+                                        vis);
+}
+
+void
+nsMenuObject::UpdateSensitivity()
+{
+    bool disabled = mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+                                                       nsGkAtoms::disabled,
+                                                       nsGkAtoms::_true,
+                                                       eCaseMatters);
+
+    dbusmenu_menuitem_property_set_bool(mNativeData,
+                                        DBUSMENU_MENUITEM_PROP_ENABLED,
+                                        !disabled);
+
+}
+
+void
+nsMenuObject::UpdateIcon(const ComputedStyle *aComputedStyle)
+{
+    if (ShouldShowIcon()) {
+        if (!mIconLoader) {
+            mIconLoader = new nsMenuObjectIconLoader(this);
+        }
+
+        mIconLoader->LoadIcon(aComputedStyle);
+    } else {
+        if (mIconLoader) {
+            mIconLoader->Destroy();
+            mIconLoader = nullptr;
+        }
+
+        ClearIcon();
+    }
+}
+
+already_AddRefed<const ComputedStyle>
+nsMenuObject::GetComputedStyle()
+{
+    RefPtr<const ComputedStyle> style =
+        nsComputedDOMStyle::GetComputedStyleNoFlush(
+            mContent->AsElement());
+
+    return style.forget();
+}
+
+void
+nsMenuObject::InitializeNativeData()
+{
+}
+
+nsMenuObject::PropertyFlags
+nsMenuObject::SupportedProperties() const
+{
+    return static_cast<nsMenuObject::PropertyFlags>(0);
+}
+
+bool
+nsMenuObject::IsCompatibleWithNativeData(DbusmenuMenuitem *aNativeData) const
+{
+    return true;
+}
+
+void
+nsMenuObject::UpdateContentAttributes()
+{
+}
+
+void
+nsMenuObject::Update(const ComputedStyle *aComputedStyle)
+{
+}
+
+bool
+nsMenuObject::ShouldShowIcon() const
+{
+    // Ideally we want to know the visibility of the anonymous XUL image in
+    // our menuitem, but this isn't created because we don't have a frame.
+    // The following works by default (because xul.css hides images in menuitems
+    // that don't have the "menuitem-with-favicon" class). It's possible a third
+    // party theme could override this, but, oh well...
+    const nsAttrValue *classes = mContent->AsElement()->GetClasses();
+    if (!classes) {
+        return false;
+    }
+
+    for (uint32_t i = 0; i < classes->GetAtomCount(); ++i) {
+        if (classes->AtomAt(i) == nsGkAtoms::menuitem_with_favicon) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+void
+nsMenuObject::ClearIcon()
+{
+    dbusmenu_menuitem_property_remove(mNativeData,
+                                      DBUSMENU_MENUITEM_PROP_ICON_DATA);
+}
+
+nsMenuObject::~nsMenuObject()
+{
+    nsWeakMenuObject::NotifyDestroyed(this);
+
+    if (mIconLoader) {
+        mIconLoader->Destroy();
+    }
+
+    if (mListener) {
+        mListener->UnregisterForContentChanges(mContent);
+    }
+
+    if (mNativeData) {
+        g_object_unref(mNativeData);
+        mNativeData = nullptr;
+    }
+}
+
+void
+nsMenuObject::CreateNativeData()
+{
+    MOZ_ASSERT(mNativeData == nullptr, "This node already has a DbusmenuMenuitem. The old one will be leaked");
+
+    mNativeData = dbusmenu_menuitem_new();
+    InitializeNativeData();
+    if (mParent && mParent->IsBeingDisplayed()) {
+        ContainerIsOpening();
+    }
+
+    mListener->RegisterForContentChanges(mContent, this);
+}
+
+nsresult
+nsMenuObject::AdoptNativeData(DbusmenuMenuitem *aNativeData)
+{
+    MOZ_ASSERT(mNativeData == nullptr, "This node already has a DbusmenuMenuitem. The old one will be leaked");
+
+    if (!IsCompatibleWithNativeData(aNativeData)) {
+        return NS_ERROR_FAILURE;
+    }
+
+    mNativeData = aNativeData;
+    g_object_ref(mNativeData);
+
+    PropertyFlags supported = SupportedProperties();
+    PropertyFlags mask = static_cast<PropertyFlags>(1);
+
+    for (uint32_t i = 0; gPropertyStrings[i]; ++i) {
+        if (!(mask & supported)) {
+            dbusmenu_menuitem_property_remove(mNativeData, gPropertyStrings[i]);
+        }
+        mask = static_cast<PropertyFlags>(mask << 1);
+    }
+
+    InitializeNativeData();
+    if (mParent && mParent->IsBeingDisplayed()) {
+        ContainerIsOpening();
+    }
+
+    mListener->RegisterForContentChanges(mContent, this);
+
+    return NS_OK;
+}
+
+void
+nsMenuObject::ContainerIsOpening()
+{
+    MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
+
+    UpdateContentAttributes();
+
+    RefPtr<const ComputedStyle> style = GetComputedStyle();
+    Update(style);
+}
+
+/* static */ void
+nsWeakMenuObject::AddWeakReference(nsWeakMenuObject *aWeak)
+{
+    aWeak->mPrev = sHead;
+    sHead = aWeak;
+}
+
+/* static */ void
+nsWeakMenuObject::RemoveWeakReference(nsWeakMenuObject *aWeak)
+{
+    if (aWeak == sHead) {
+        sHead = aWeak->mPrev;
+        return;
+    }
+
+    nsWeakMenuObject *weak = sHead;
+    while (weak && weak->mPrev != aWeak) {
+        weak = weak->mPrev;
+    }
+
+    if (weak) {
+        weak->mPrev = aWeak->mPrev;
+    }
+}
+
+/* static */ void
+nsWeakMenuObject::NotifyDestroyed(nsMenuObject *aMenuObject)
+{
+    nsWeakMenuObject *weak = sHead;
+    while (weak) {
+        if (weak->mMenuObject == aMenuObject) {
+            weak->mMenuObject = nullptr;
+        }
+
+        weak = weak->mPrev;
+    }
+}
--- /dev/null
+++ b/widget/gtk/nsMenuObject.h
@@ -0,0 +1,169 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsMenuObject_h__
+#define __nsMenuObject_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ComputedStyleInlines.h"
+#include "nsCOMPtr.h"
+
+#include "nsDbusmenu.h"
+#include "nsNativeMenuDocListener.h"
+
+class nsIContent;
+class nsMenuContainer;
+class nsMenuObjectIconLoader;
+
+#define DBUSMENU_PROPERTIES \
+    DBUSMENU_PROPERTY(Label, DBUSMENU_MENUITEM_PROP_LABEL, 0) \
+    DBUSMENU_PROPERTY(Enabled, DBUSMENU_MENUITEM_PROP_ENABLED, 1) \
+    DBUSMENU_PROPERTY(Visible, DBUSMENU_MENUITEM_PROP_VISIBLE, 2) \
+    DBUSMENU_PROPERTY(IconData, DBUSMENU_MENUITEM_PROP_ICON_DATA, 3) \
+    DBUSMENU_PROPERTY(Type, DBUSMENU_MENUITEM_PROP_TYPE, 4) \
+    DBUSMENU_PROPERTY(Shortcut, DBUSMENU_MENUITEM_PROP_SHORTCUT, 5) \
+    DBUSMENU_PROPERTY(ToggleType, DBUSMENU_MENUITEM_PROP_TOGGLE_TYPE, 6) \
+    DBUSMENU_PROPERTY(ToggleState, DBUSMENU_MENUITEM_PROP_TOGGLE_STATE, 7) \
+    DBUSMENU_PROPERTY(ChildDisplay, DBUSMENU_MENUITEM_PROP_CHILD_DISPLAY, 8)
+
+/*
+ * This is the base class for all menu nodes. Each instance represents
+ * a single node in the menu hierarchy. It wraps the corresponding DOM node and
+ * native menu node, keeps them in sync and transfers events between the two.
+ * It is not reference counted - each node is owned by its parent (the top
+ * level menubar is owned by the window) and keeps a weak pointer to its
+ * parent (which is guaranteed to always be valid because a node will never
+ * outlive its parent). It is not safe to keep a reference to nsMenuObject
+ * externally.
+ */
+class nsMenuObject : public nsNativeMenuChangeObserver
+{
+public:
+    enum EType {
+        eType_MenuBar,
+        eType_Menu,
+        eType_MenuItem
+    };
+
+    virtual ~nsMenuObject();
+
+    // Get the native menu item node
+    DbusmenuMenuitem* GetNativeData() const { return mNativeData; }
+
+    // Get the parent menu object
+    nsMenuContainer* Parent() const { return mParent; }
+
+    // Get the content node
+    nsIContent* ContentNode() const { return mContent; }
+
+    // Get the type of this node. Must be provided by subclasses
+    virtual EType Type() const = 0;
+
+    // Get the document listener
+    nsNativeMenuDocListener* DocListener() const { return mListener; }
+
+    // Create the native menu item node (called by containers)
+    void CreateNativeData();
+
+    // Adopt the specified native menu item node (called by containers)
+    nsresult AdoptNativeData(DbusmenuMenuitem *aNativeData);
+
+    // Called by the container to tell us that it's opening
+    void ContainerIsOpening();
+
+protected:
+    nsMenuObject(nsMenuContainer *aParent, nsIContent *aContent);
+    nsMenuObject(nsNativeMenuDocListener *aListener, nsIContent *aContent);
+
+    enum PropertyFlags {
+#define DBUSMENU_PROPERTY(e, s, b) eProp##e = (1 << b),
+        DBUSMENU_PROPERTIES
+#undef DBUSMENU_PROPERTY
+    };
+
+    void UpdateLabel();
+    void UpdateVisibility(const mozilla::ComputedStyle *aComputedStyle);
+    void UpdateSensitivity();
+    void UpdateIcon(const mozilla::ComputedStyle *aComputedStyle);
+
+    already_AddRefed<const mozilla::ComputedStyle> GetComputedStyle();
+
+private:
+    friend class nsMenuObjectIconLoader;
+
+    // Set up initial properties on the native data, connect to signals etc.
+    // This should be implemented by subclasses
+    virtual void InitializeNativeData();
+
+    // Return the properties that this menu object type supports
+    // This should be implemented by subclasses
+    virtual PropertyFlags SupportedProperties() const;
+
+    // Determine whether this menu object could use the specified
+    // native item. Returns true by default but can be overridden by subclasses
+    virtual bool
+    IsCompatibleWithNativeData(DbusmenuMenuitem *aNativeData) const;
+
+    // Update attributes on this objects content node when the container opens.
+    // This is called before style resolution, and should be implemented by
+    // subclasses who want to modify attributes that might affect style.
+    // This will not be called when there are script blockers
+    virtual void UpdateContentAttributes();
+
+    // Update properties that should be refreshed when the container opens.
+    // This should be implemented by subclasses that have properties which
+    // need refreshing
+    virtual void Update(const mozilla::ComputedStyle *aComputedStyle);
+
+    bool ShouldShowIcon() const;
+    void ClearIcon();
+
+    nsCOMPtr<nsIContent> mContent;
+    // mListener is a strong ref for simplicity - someone in the tree needs to
+    // own it, and this only really needs to be the top-level object (as no
+    // children outlives their parent). However, we need to keep it alive until
+    // after running the nsMenuObject destructor for the top-level menu object,
+    // hence the strong ref
+    RefPtr<nsNativeMenuDocListener> mListener;
+    nsMenuContainer *mParent; // [weak]
+    DbusmenuMenuitem *mNativeData; // [strong]
+    RefPtr<nsMenuObjectIconLoader> mIconLoader;
+};
+
+// Keep a weak pointer to a menu object
+class nsWeakMenuObject
+{
+public:
+    nsWeakMenuObject() : mPrev(nullptr), mMenuObject(nullptr) {}
+
+    nsWeakMenuObject(nsMenuObject *aMenuObject) :
+        mPrev(nullptr), mMenuObject(aMenuObject)
+    {
+        AddWeakReference(this);
+    }
+
+    ~nsWeakMenuObject() { RemoveWeakReference(this); }
+
+    nsMenuObject* get() const { return mMenuObject; }
+
+    nsMenuObject* operator->() const { return mMenuObject; }
+
+    explicit operator bool() const { return !!mMenuObject; }
+
+    static void NotifyDestroyed(nsMenuObject *aMenuObject);
+
+private:
+    static void AddWeakReference(nsWeakMenuObject *aWeak);
+    static void RemoveWeakReference(nsWeakMenuObject *aWeak);
+
+    nsWeakMenuObject *mPrev;
+    static nsWeakMenuObject *sHead;
+
+    nsMenuObject *mMenuObject;
+};
+
+#endif /* __nsMenuObject_h__ */
--- /dev/null
+++ b/widget/gtk/nsMenuSeparator.cpp
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#include "mozilla/Assertions.h"
+#include "nsCRT.h"
+#include "nsGkAtoms.h"
+
+#include "nsDbusmenu.h"
+
+#include "nsMenuContainer.h"
+#include "nsMenuSeparator.h"
+
+using namespace mozilla;
+
+void
+nsMenuSeparator::InitializeNativeData()
+{
+    dbusmenu_menuitem_property_set(GetNativeData(),
+                                   DBUSMENU_MENUITEM_PROP_TYPE,
+                                   "separator");
+}
+
+void
+nsMenuSeparator::Update(const ComputedStyle *aComputedStyle)
+{
+    UpdateVisibility(aComputedStyle);
+}
+
+bool
+nsMenuSeparator::IsCompatibleWithNativeData(DbusmenuMenuitem *aNativeData) const
+{
+    return nsCRT::strcmp(dbusmenu_menuitem_property_get(aNativeData,
+                                                        DBUSMENU_MENUITEM_PROP_TYPE),
+                         "separator") == 0;
+}
+
+nsMenuObject::PropertyFlags
+nsMenuSeparator::SupportedProperties() const
+{
+    return static_cast<nsMenuObject::PropertyFlags>(
+        nsMenuObject::ePropVisible |
+        nsMenuObject::ePropType
+    );
+}
+
+void
+nsMenuSeparator::OnAttributeChanged(nsIContent *aContent, nsAtom *aAttribute)
+{
+    MOZ_ASSERT(aContent == ContentNode(), "Received an event that wasn't meant for us!");
+
+    if (!Parent()->IsBeingDisplayed()) {
+        return;
+    }
+
+    if (aAttribute == nsGkAtoms::hidden ||
+        aAttribute == nsGkAtoms::collapsed) {
+        RefPtr<const ComputedStyle> style = GetComputedStyle();
+        UpdateVisibility(style);
+    }
+}
+
+nsMenuSeparator::nsMenuSeparator(nsMenuContainer *aParent,
+                                 nsIContent *aContent) :
+    nsMenuObject(aParent, aContent)
+{
+    MOZ_COUNT_CTOR(nsMenuSeparator);
+}
+
+nsMenuSeparator::~nsMenuSeparator()
+{
+    MOZ_COUNT_DTOR(nsMenuSeparator);
+}
+
+nsMenuObject::EType
+nsMenuSeparator::Type() const
+{
+    return eType_MenuItem;
+}
--- /dev/null
+++ b/widget/gtk/nsMenuSeparator.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsMenuSeparator_h__
+#define __nsMenuSeparator_h__
+
+#include "mozilla/Attributes.h"
+
+#include "nsMenuObject.h"
+
+class nsIContent;
+class nsAtom;
+class nsMenuContainer;
+
+// Menu separator class
+class nsMenuSeparator final : public nsMenuObject
+{
+public:
+    nsMenuSeparator(nsMenuContainer *aParent, nsIContent *aContent);
+    ~nsMenuSeparator();
+
+    nsMenuObject::EType Type() const override;
+
+private:
+    void InitializeNativeData() override;
+    void Update(const mozilla::ComputedStyle *aComputedStyle) override;
+    bool IsCompatibleWithNativeData(DbusmenuMenuitem *aNativeData) const override;
+    nsMenuObject::PropertyFlags SupportedProperties() const override;
+
+    void OnAttributeChanged(nsIContent *aContent, nsAtom *aAttribute) override;
+};
+
+#endif /* __nsMenuSeparator_h__ */
--- /dev/null
+++ b/widget/gtk/nsNativeMenuDocListener.cpp
@@ -0,0 +1,347 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#include "mozilla/Assertions.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "nsContentUtils.h"
+#include "nsAtom.h"
+#include "nsIContent.h"
+
+#include "nsMenuContainer.h"
+
+#include "nsNativeMenuDocListener.h"
+
+using namespace mozilla;
+
+uint32_t nsNativeMenuDocListener::sUpdateBlockersCount = 0;
+
+nsNativeMenuDocListenerTArray *gPendingListeners;
+
+/*
+ * Small helper which caches a single listener, so that consecutive
+ * events which go to the same node avoid multiple hash table lookups
+ */
+class MOZ_STACK_CLASS DispatchHelper
+{
+public:
+    DispatchHelper(nsNativeMenuDocListener *aListener,
+                   nsIContent *aContent) :
+                   mObserver(nullptr)
+    {
+        if (aContent == aListener->mLastSource) {
+            mObserver = aListener->mLastTarget;
+        } else {
+            mObserver = aListener->mContentToObserverTable.Get(aContent);
+            if (mObserver) {
+                aListener->mLastSource = aContent;
+                aListener->mLastTarget = mObserver;
+            }
+        }
+    }
+
+    ~DispatchHelper() { };
+
+    nsNativeMenuChangeObserver* Observer() const { return mObserver; }
+
+    bool HasObserver() const { return !!mObserver; }
+
+private:
+    nsNativeMenuChangeObserver *mObserver;
+};
+
+NS_IMPL_ISUPPORTS(nsNativeMenuDocListener, nsIMutationObserver)
+
+nsNativeMenuDocListener::~nsNativeMenuDocListener()
+{
+    MOZ_ASSERT(mContentToObserverTable.Count() == 0,
+               "Some nodes forgot to unregister listeners. This is bad! (and we're lucky we made it this far)");
+    MOZ_COUNT_DTOR(nsNativeMenuDocListener);
+}
+
+void
+nsNativeMenuDocListener::AttributeChanged(mozilla::dom::Element *aElement,
+                                          int32_t aNameSpaceID,
+                                          nsAtom *aAttribute,
+                                          int32_t aModType,
+                                          const nsAttrValue* aOldValue)
+{
+    if (sUpdateBlockersCount == 0) {
+        DoAttributeChanged(aElement, aAttribute);
+        return;
+    }
+
+    MutationRecord *m = mPendingMutations.AppendElement(MakeUnique<MutationRecord>())->get();
+    m->mType = MutationRecord::eAttributeChanged;
+    m->mTarget = aElement;
+    m->mAttribute = aAttribute;
+
+    ScheduleFlush(this);
+}
+
+void
+nsNativeMenuDocListener::ContentAppended(nsIContent *aFirstNewContent)
+{
+    for (nsIContent *c = aFirstNewContent; c; c = c->GetNextSibling()) {
+        ContentInserted(c);
+    }
+}
+
+void
+nsNativeMenuDocListener::ContentInserted(nsIContent *aChild)
+{
+    nsIContent* container = aChild->GetParent();
+    if (!container) {
+      return;
+    }
+
+    nsIContent *prevSibling = nsMenuContainer::GetPreviousSupportedSibling(aChild);
+
+    if (sUpdateBlockersCount == 0) {
+        DoContentInserted(container, aChild, prevSibling);
+        return;
+    }
+
+    MutationRecord *m = mPendingMutations.AppendElement(MakeUnique<MutationRecord>())->get();
+    m->mType = MutationRecord::eContentInserted;
+    m->mTarget = container;
+    m->mChild = aChild;
+    m->mPrevSibling = prevSibling;
+
+    ScheduleFlush(this);
+}
+
+void
+nsNativeMenuDocListener::ContentRemoved(nsIContent *aChild,
+                                        nsIContent *aPreviousSibling)
+{
+    nsIContent* container = aChild->GetParent();
+    if (!container) {
+      return;
+    }
+
+    if (sUpdateBlockersCount == 0) {
+        DoContentRemoved(container, aChild);
+        return;
+    }
+
+    MutationRecord *m = mPendingMutations.AppendElement(MakeUnique<MutationRecord>())->get();
+    m->mType = MutationRecord::eContentRemoved;
+    m->mTarget = container;
+    m->mChild = aChild;
+
+    ScheduleFlush(this);
+}
+
+void
+nsNativeMenuDocListener::NodeWillBeDestroyed(nsINode *aNode)
+{
+    mDocument = nullptr;
+}
+
+void
+nsNativeMenuDocListener::DoAttributeChanged(nsIContent *aContent,
+                                            nsAtom *aAttribute)
+{
+    DispatchHelper h(this, aContent);
+    if (h.HasObserver()) {
+        h.Observer()->OnAttributeChanged(aContent, aAttribute);
+    }
+}
+
+void
+nsNativeMenuDocListener::DoContentInserted(nsIContent *aContainer,
+                                           nsIContent *aChild,
+                                           nsIContent *aPrevSibling)
+{
+    DispatchHelper h(this, aContainer);
+    if (h.HasObserver()) {
+        h.Observer()->OnContentInserted(aContainer, aChild, aPrevSibling);
+    }
+}
+
+void
+nsNativeMenuDocListener::DoContentRemoved(nsIContent *aContainer,
+                                          nsIContent *aChild)
+{
+    DispatchHelper h(this, aContainer);
+    if (h.HasObserver()) {
+        h.Observer()->OnContentRemoved(aContainer, aChild);
+    }
+}
+
+void
+nsNativeMenuDocListener::DoBeginUpdates(nsIContent *aTarget)
+{
+    DispatchHelper h(this, aTarget);
+    if (h.HasObserver()) {
+        h.Observer()->OnBeginUpdates(aTarget);
+    }
+}
+
+void
+nsNativeMenuDocListener::DoEndUpdates(nsIContent *aTarget)
+{
+    DispatchHelper h(this, aTarget);
+    if (h.HasObserver()) {
+        h.Observer()->OnEndUpdates();
+    }
+}
+
+void
+nsNativeMenuDocListener::FlushPendingMutations()
+{
+    nsIContent *currentTarget = nullptr;
+    bool inUpdateSequence = false;
+
+    while (mPendingMutations.Length() > 0) {
+        MutationRecord *m = mPendingMutations[0].get();
+
+        if (m->mTarget != currentTarget) {
+            if (inUpdateSequence) {
+                DoEndUpdates(currentTarget);
+                inUpdateSequence = false;
+            }
+
+            currentTarget = m->mTarget;
+
+            if (mPendingMutations.Length() > 1 &&
+                mPendingMutations[1]->mTarget == currentTarget) {
+                DoBeginUpdates(currentTarget);
+                inUpdateSequence = true;
+            }
+        }
+
+        switch (m->mType) {
+            case MutationRecord::eAttributeChanged:
+                DoAttributeChanged(m->mTarget, m->mAttribute);
+                break;
+            case MutationRecord::eContentInserted:
+                DoContentInserted(m->mTarget, m->mChild, m->mPrevSibling);
+                break;
+            case MutationRecord::eContentRemoved:
+                DoContentRemoved(m->mTarget, m->mChild);
+                break;
+            default:
+                MOZ_ASSERT_UNREACHABLE("Invalid type");
+        }
+
+        mPendingMutations.RemoveElementAt(0);
+    }
+
+    if (inUpdateSequence) {
+        DoEndUpdates(currentTarget);
+    }
+}
+
+/* static */ void
+nsNativeMenuDocListener::ScheduleFlush(nsNativeMenuDocListener *aListener)
+{
+    MOZ_ASSERT(sUpdateBlockersCount > 0, "Shouldn't be doing this now");
+
+    if (!gPendingListeners) {
+        gPendingListeners = new nsNativeMenuDocListenerTArray;
+    }
+
+    if (gPendingListeners->IndexOf(aListener) ==
+        nsNativeMenuDocListenerTArray::NoIndex) {
+        gPendingListeners->AppendElement(aListener);
+    }
+}
+
+/* static */ void
+nsNativeMenuDocListener::CancelFlush(nsNativeMenuDocListener *aListener)
+{
+    if (!gPendingListeners) {
+        return;
+    }
+
+    gPendingListeners->RemoveElement(aListener);
+}
+
+/* static */ void
+nsNativeMenuDocListener::RemoveUpdateBlocker()
+{
+    if (sUpdateBlockersCount == 1 && gPendingListeners) {
+        while (gPendingListeners->Length() > 0) {
+            (*gPendingListeners)[0]->FlushPendingMutations();
+            gPendingListeners->RemoveElementAt(0);
+        }
+    }
+
+    MOZ_ASSERT(sUpdateBlockersCount > 0, "Negative update blockers count!");
+    sUpdateBlockersCount--;
+}
+
+nsNativeMenuDocListener::nsNativeMenuDocListener(nsIContent *aRootNode) :
+    mRootNode(aRootNode),
+    mDocument(nullptr),
+    mLastSource(nullptr),
+    mLastTarget(nullptr)
+{
+    MOZ_COUNT_CTOR(nsNativeMenuDocListener);
+}
+
+void
+nsNativeMenuDocListener::RegisterForContentChanges(nsIContent *aContent,
+                                                   nsNativeMenuChangeObserver *aObserver)
+{
+    MOZ_ASSERT(aContent, "Need content parameter");
+    MOZ_ASSERT(aObserver, "Need observer parameter");
+    if (!aContent || !aObserver) {
+        return;
+    }
+
+    DebugOnly<nsNativeMenuChangeObserver *> old;
+    MOZ_ASSERT(!mContentToObserverTable.Get(aContent, &old) || old == aObserver,
+               "Multiple observers for the same content node are not supported");
+
+    mContentToObserverTable.InsertOrUpdate(aContent, aObserver);
+}
+
+void
+nsNativeMenuDocListener::UnregisterForContentChanges(nsIContent *aContent)
+{
+    MOZ_ASSERT(aContent, "Need content parameter");
+    if (!aContent) {
+        return;
+    }
+
+    mContentToObserverTable.Remove(aContent);
+    if (aContent == mLastSource) {
+        mLastSource = nullptr;
+        mLastTarget = nullptr;
+    }
+}
+
+void
+nsNativeMenuDocListener::Start()
+{
+    if (mDocument) {
+        return;
+    }
+
+    mDocument = mRootNode->OwnerDoc();
+    if (!mDocument) {
+        return;
+    }
+
+    mDocument->AddMutationObserver(this);
+}
+
+void
+nsNativeMenuDocListener::Stop()
+{
+    if (mDocument) {
+        mDocument->RemoveMutationObserver(this);
+        mDocument = nullptr;
+    }
+
+    CancelFlush(this);
+    mPendingMutations.Clear();
+}
--- /dev/null
+++ b/widget/gtk/nsNativeMenuDocListener.h
@@ -0,0 +1,152 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsNativeMenuDocListener_h__
+#define __nsNativeMenuDocListener_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsTHashMap.h"
+#include "nsStubMutationObserver.h"
+#include "nsTArray.h"
+
+class nsAtom;
+class nsIContent;
+class nsNativeMenuChangeObserver;
+
+namespace mozilla {
+namespace dom {
+class Document;
+}
+}
+
+/*
+ * This class keeps a mapping of content nodes to observers and forwards DOM
+ * mutations to these. There is exactly one of these for every menubar.
+ */
+class nsNativeMenuDocListener final : nsStubMutationObserver
+{
+public:
+    NS_DECL_ISUPPORTS
+
+    nsNativeMenuDocListener(nsIContent *aRootNode);
+
+    // Register an observer to receive mutation events for the specified
+    // content node. The caller must keep the observer alive until
+    // UnregisterForContentChanges is called.
+    void RegisterForContentChanges(nsIContent *aContent,
+                                   nsNativeMenuChangeObserver *aObserver);
+
+    // Unregister the registered observer for the specified content node
+    void UnregisterForContentChanges(nsIContent *aContent);
+
+    // Start listening to the document and forwarding DOM mutations to
+    // registered observers.
+    void Start();
+
+    // Stop listening to the document. No DOM mutations will be forwarded
+    // to registered observers.
+    void Stop();
+
+    /*
+     * This class is intended to be used inside GObject signal handlers.
+     * It allows us to queue updates until we have finished delivering
+     * events to Gecko, and then we can batch updates to our view of the
+     * menu. This allows us to do menu updates without altering the structure
+     * seen by the OS.
+     */
+    class MOZ_STACK_CLASS BlockUpdatesScope
+    {
+    public:
+        BlockUpdatesScope()
+        {
+            nsNativeMenuDocListener::AddUpdateBlocker();
+        }
+
+        ~BlockUpdatesScope()
+        {
+            nsNativeMenuDocListener::RemoveUpdateBlocker();
+        }
+    };
+
+private:
+    friend class DispatchHelper;
+
+    struct MutationRecord {
+        enum RecordType {
+            eAttributeChanged,
+            eContentInserted,
+            eContentRemoved
+        } mType;
+
+        nsCOMPtr<nsIContent> mTarget;
+        nsCOMPtr<nsIContent> mChild;
+        nsCOMPtr<nsIContent> mPrevSibling;
+        RefPtr<nsAtom> mAttribute;
+    };
+
+    ~nsNativeMenuDocListener();
+
+    NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
+    NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+    NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+    NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+    NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED
+
+    void DoAttributeChanged(nsIContent *aContent, nsAtom *aAttribute);
+    void DoContentInserted(nsIContent *aContainer,
+                           nsIContent *aChild,
+                           nsIContent *aPrevSibling);
+    void DoContentRemoved(nsIContent *aContainer, nsIContent *aChild);
+    void DoBeginUpdates(nsIContent *aTarget);
+    void DoEndUpdates(nsIContent *aTarget);
+
+    void FlushPendingMutations();
+    static void ScheduleFlush(nsNativeMenuDocListener *aListener);
+    static void CancelFlush(nsNativeMenuDocListener *aListener);
+
+    static void AddUpdateBlocker() { ++sUpdateBlockersCount; }
+    static void RemoveUpdateBlocker();
+
+    nsCOMPtr<nsIContent> mRootNode;
+    mozilla::dom::Document *mDocument;
+    nsIContent *mLastSource;
+    nsNativeMenuChangeObserver *mLastTarget;
+    nsTArray<mozilla::UniquePtr<MutationRecord> > mPendingMutations;
+    nsTHashMap<nsPtrHashKey<nsIContent>, nsNativeMenuChangeObserver *> mContentToObserverTable;
+
+    static uint32_t sUpdateBlockersCount;
+};
+
+typedef nsTArray<RefPtr<nsNativeMenuDocListener> > nsNativeMenuDocListenerTArray;
+
+/*
+ * Implemented by classes that want to listen to mutation events from content
+ * nodes.
+ */
+class nsNativeMenuChangeObserver
+{
+public:
+    virtual void OnAttributeChanged(nsIContent *aContent, nsAtom *aAttribute) {}
+
+    virtual void OnContentInserted(nsIContent *aContainer,
+                                   nsIContent *aChild,
+                                   nsIContent *aPrevSibling) {}
+
+    virtual void OnContentRemoved(nsIContent *aContainer, nsIContent *aChild) {}
+
+    // Signals the start of a sequence of more than 1 event for the specified
+    // node. This only happens when events are flushed as all BlockUpdatesScope
+    // instances go out of scope
+    virtual void OnBeginUpdates(nsIContent *aContent) {};
+
+    // Signals the end of a sequence of events
+    virtual void OnEndUpdates() {};
+};
+
+#endif /* __nsNativeMenuDocListener_h__ */
--- /dev/null
+++ b/widget/gtk/nsNativeMenuService.cpp
@@ -0,0 +1,478 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "nsCRT.h"
+#include "nsGtkUtils.h"
+#include "nsIContent.h"
+#include "nsIWidget.h"
+#include "nsServiceManagerUtils.h"
+#include "nsWindow.h"
+#include "prlink.h"
+
+#include "nsDbusmenu.h"
+#include "nsMenuBar.h"
+#include "nsNativeMenuDocListener.h"
+
+#include <glib-object.h>
+#include <pango/pango.h>
+#include <stdlib.h>
+
+#include "nsNativeMenuService.h"
+
+using namespace mozilla;
+
+nsNativeMenuService* nsNativeMenuService::sService = nullptr;
+
+extern PangoLayout* gPangoLayout;
+extern nsNativeMenuDocListenerTArray* gPendingListeners;
+
+#undef g_dbus_proxy_new_for_bus
+#undef g_dbus_proxy_new_for_bus_finish
+#undef g_dbus_proxy_call
+#undef g_dbus_proxy_call_finish
+#undef g_dbus_proxy_get_name_owner
+
+typedef void (*_g_dbus_proxy_new_for_bus_fn)(GBusType, GDBusProxyFlags,
+                                             GDBusInterfaceInfo*,
+                                             const gchar*, const gchar*,
+                                             const gchar*, GCancellable*,
+                                             GAsyncReadyCallback, gpointer);
+
+typedef GDBusProxy* (*_g_dbus_proxy_new_for_bus_finish_fn)(GAsyncResult*,
+                                                           GError**);
+typedef void (*_g_dbus_proxy_call_fn)(GDBusProxy*, const gchar*, GVariant*,
+                                      GDBusCallFlags, gint, GCancellable*,
+                                      GAsyncReadyCallback, gpointer);
+typedef GVariant* (*_g_dbus_proxy_call_finish_fn)(GDBusProxy*, GAsyncResult*,
+                                                  GError**);
+typedef gchar* (*_g_dbus_proxy_get_name_owner_fn)(GDBusProxy*);
+
+static _g_dbus_proxy_new_for_bus_fn _g_dbus_proxy_new_for_bus;
+static _g_dbus_proxy_new_for_bus_finish_fn _g_dbus_proxy_new_for_bus_finish;
+static _g_dbus_proxy_call_fn _g_dbus_proxy_call;
+static _g_dbus_proxy_call_finish_fn _g_dbus_proxy_call_finish;
+static _g_dbus_proxy_get_name_owner_fn _g_dbus_proxy_get_name_owner;
+
+#define g_dbus_proxy_new_for_bus _g_dbus_proxy_new_for_bus
+#define g_dbus_proxy_new_for_bus_finish _g_dbus_proxy_new_for_bus_finish
+#define g_dbus_proxy_call _g_dbus_proxy_call
+#define g_dbus_proxy_call_finish _g_dbus_proxy_call_finish
+#define g_dbus_proxy_get_name_owner _g_dbus_proxy_get_name_owner
+
+static PRLibrary *gGIOLib = nullptr;
+
+static nsresult
+GDBusInit()
+{
+    gGIOLib = PR_LoadLibrary("libgio-2.0.so.0");
+    if (!gGIOLib) {
+        return NS_ERROR_FAILURE;
+    }
+
+    g_dbus_proxy_new_for_bus = (_g_dbus_proxy_new_for_bus_fn)PR_FindFunctionSymbol(gGIOLib, "g_dbus_proxy_new_for_bus");
+    g_dbus_proxy_new_for_bus_finish = (_g_dbus_proxy_new_for_bus_finish_fn)PR_FindFunctionSymbol(gGIOLib, "g_dbus_proxy_new_for_bus_finish");
+    g_dbus_proxy_call = (_g_dbus_proxy_call_fn)PR_FindFunctionSymbol(gGIOLib, "g_dbus_proxy_call");
+    g_dbus_proxy_call_finish = (_g_dbus_proxy_call_finish_fn)PR_FindFunctionSymbol(gGIOLib, "g_dbus_proxy_call_finish");
+    g_dbus_proxy_get_name_owner = (_g_dbus_proxy_get_name_owner_fn)PR_FindFunctionSymbol(gGIOLib, "g_dbus_proxy_get_name_owner");
+
+    if (!g_dbus_proxy_new_for_bus ||
+        !g_dbus_proxy_new_for_bus_finish ||
+        !g_dbus_proxy_call ||
+        !g_dbus_proxy_call_finish ||
+        !g_dbus_proxy_get_name_owner) {
+        return NS_ERROR_FAILURE;
+    }
+
+    return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsNativeMenuService, nsINativeMenuService)
+
+nsNativeMenuService::nsNativeMenuService() :
+    mCreateProxyCancellable(nullptr), mDbusProxy(nullptr), mOnline(false)
+{
+}
+
+nsNativeMenuService::~nsNativeMenuService()
+{
+    SetOnline(false);
+
+    if (mCreateProxyCancellable) {
+        g_cancellable_cancel(mCreateProxyCancellable);
+        g_object_unref(mCreateProxyCancellable);
+        mCreateProxyCancellable = nullptr;
+    }
+
+    // Make sure we disconnect map-event handlers
+    while (mMenuBars.Length() > 0) {
+        NotifyNativeMenuBarDestroyed(mMenuBars[0]);
+    }
+
+    Preferences::UnregisterCallback(PrefChangedCallback,
+                                    "ui.use_unity_menubar");
+
+    if (mDbusProxy) {
+        g_signal_handlers_disconnect_by_func(mDbusProxy,
+                                             FuncToGpointer(name_owner_changed_cb),
+                                             NULL);
+        g_object_unref(mDbusProxy);
+    }
+
+    if (gPendingListeners) {
+        delete gPendingListeners;
+        gPendingListeners = nullptr;
+    }
+    if (gPangoLayout) {
+        g_object_unref(gPangoLayout);
+        gPangoLayout = nullptr;
+    }
+
+    MOZ_ASSERT(sService == this);
+    sService = nullptr;
+}
+
+nsresult
+nsNativeMenuService::Init()
+{
+    nsresult rv = nsDbusmenuFunctions::Init();
+    if (NS_FAILED(rv)) {
+        return rv;
+    }
+
+    rv = GDBusInit();
+    if (NS_FAILED(rv)) {
+        return rv;
+    }
+
+    Preferences::RegisterCallback(PrefChangedCallback,
+                                  "ui.use_unity_menubar");
+
+    mCreateProxyCancellable = g_cancellable_new();
+
+    g_dbus_proxy_new_for_bus(G_BUS_TYPE_SESSION,
+                             static_cast<GDBusProxyFlags>(
+                                 G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES |
+                                 G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS |
+                                 G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START),
+                             nullptr,
+                             "com.canonical.AppMenu.Registrar",
+                             "/com/canonical/AppMenu/Registrar",
+                             "com.canonical.AppMenu.Registrar",
+                             mCreateProxyCancellable, proxy_created_cb,
+                             nullptr);
+
+    /* We don't technically know that the shell will draw the menubar until
+     * we know whether anybody owns the name of the menubar service on the
+     * session bus. However, discovering this happens asynchronously so
+     * we optimize for the common case here by assuming that the shell will
+     * draw window menubars if we are running inside Unity. This should
+     * mean that we avoid temporarily displaying the window menubar ourselves
+     */
+    const char *desktop = getenv("XDG_CURRENT_DESKTOP");
+    if (nsCRT::strcmp(desktop, "Unity") == 0) {
+        SetOnline(true);
+    }
+
+    return NS_OK;
+}
+
+/* static */ void
+nsNativeMenuService::EnsureInitialized()
+{
+    if (sService) {
+        return;
+    }
+    nsCOMPtr<nsINativeMenuService> service =
+        do_GetService("@mozilla.org/widget/nativemenuservice;1");
+}
+
+void
+nsNativeMenuService::SetOnline(bool aOnline)
+{
+    if (!Preferences::GetBool("ui.use_unity_menubar", true)) {
+        aOnline = false;
+    }
+
+    mOnline = aOnline;
+    if (aOnline) {
+        for (uint32_t i = 0; i < mMenuBars.Length(); ++i) {
+            RegisterNativeMenuBar(mMenuBars[i]);
+        }
+    } else {
+        for (uint32_t i = 0; i < mMenuBars.Length(); ++i) {
+            mMenuBars[i]->Deactivate();
+        }
+    }
+}
+
+void
+nsNativeMenuService::RegisterNativeMenuBar(nsMenuBar *aMenuBar)
+{
+    if (!mOnline) {
+        return;
+    }
+
+    // This will effectively create the native menubar for
+    // exporting over the session bus, and hide the XUL menubar
+    aMenuBar->Activate();
+
+    if (!mDbusProxy ||
+        !gtk_widget_get_mapped(aMenuBar->TopLevelWindow()) ||
+        mMenuBarRegistrationCancellables.Get(aMenuBar, nullptr)) {
+        // Don't go further if we don't have a proxy for the shell menu
+        // service, the window isn't mapped or there is a request in progress.
+        return;
+    }
+
+    uint32_t xid = aMenuBar->WindowId();
+    nsCString path = aMenuBar->ObjectPath();
+    if (xid == 0 || path.IsEmpty()) {
+        NS_WARNING("Menubar has invalid XID or object path");
+        return;
+    }
+
+    GCancellable *cancellable = g_cancellable_new();
+    mMenuBarRegistrationCancellables.InsertOrUpdate(aMenuBar, cancellable);
+
+    // We keep a weak ref because we can't assume that GDBus cancellation
+    // is reliable (see https://launchpad.net/bugs/953562)
+
+    g_dbus_proxy_call(mDbusProxy, "RegisterWindow",
+                      g_variant_new("(uo)", xid, path.get()),
+                      G_DBUS_CALL_FLAGS_NONE, -1,
+                      cancellable,
+                      register_native_menubar_cb, aMenuBar);
+}
+
+/* static */ void
+nsNativeMenuService::name_owner_changed_cb(GObject *gobject,
+                                           GParamSpec *pspec,
+                                           gpointer user_data)
+{
+    nsNativeMenuService::GetSingleton()->OnNameOwnerChanged();
+}
+
+/* static */ void
+nsNativeMenuService::proxy_created_cb(GObject *source_object,
+                                      GAsyncResult *res,
+                                      gpointer user_data)
+{
+    GError *error = nullptr;
+    GDBusProxy *proxy = g_dbus_proxy_new_for_bus_finish(res, &error);
+    if (error && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+        g_error_free(error);
+        return;
+    }
+
+    if (error) {
+        g_error_free(error);
+    }
+
+    // We need this check because we can't assume that GDBus cancellation
+    // is reliable (see https://launchpad.net/bugs/953562)
+    nsNativeMenuService *self = nsNativeMenuService::GetSingleton();
+    if (!self) {
+        if (proxy) {
+            g_object_unref(proxy);
+        }
+        return;
+    }
+
+    self->OnProxyCreated(proxy);
+}
+
+/* static */ void
+nsNativeMenuService::register_native_menubar_cb(GObject *source_object,
+                                                GAsyncResult *res,
+                                                gpointer user_data)
+{
+    nsMenuBar *menuBar = static_cast<nsMenuBar *>(user_data);
+
+    GError *error = nullptr;
+    GVariant *results = g_dbus_proxy_call_finish(G_DBUS_PROXY(source_object),
+                                                 res, &error);
+    if (results) {
+        // There's nothing useful in the response
+        g_variant_unref(results);
+    }
+
+    bool success = error ? false : true;
+    if (error && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+        g_error_free(error);
+        return;
+    }
+
+    if (error) {
+        g_error_free(error);
+    }
+
+    nsNativeMenuService *self = nsNativeMenuService::GetSingleton();
+    if (!self) {
+        return;
+    }
+
+    self->OnNativeMenuBarRegistered(menuBar, success);
+}
+
+/* static */ gboolean
+nsNativeMenuService::map_event_cb(GtkWidget *widget,
+                                  GdkEvent *event,
+                                  gpointer user_data)
+{
+    nsMenuBar *menubar = static_cast<nsMenuBar *>(user_data);
+    nsNativeMenuService::GetSingleton()->RegisterNativeMenuBar(menubar);
+
+    return FALSE;
+}
+
+void
+nsNativeMenuService::OnNameOwnerChanged()
+{
+    char *owner = g_dbus_proxy_get_name_owner(mDbusProxy);
+    SetOnline(owner ? true : false);
+    g_free(owner);
+}
+
+void
+nsNativeMenuService::OnProxyCreated(GDBusProxy *aProxy)
+{
+    mDbusProxy = aProxy;
+
+    g_object_unref(mCreateProxyCancellable);
+    mCreateProxyCancellable = nullptr;
+
+    if (!mDbusProxy) {
+        SetOnline(false);
+        return;
+    }
+
+    g_signal_connect(mDbusProxy, "notify::g-name-owner",
+                     G_CALLBACK(name_owner_changed_cb), nullptr);
+
+    OnNameOwnerChanged();
+}
+
+void
+nsNativeMenuService::OnNativeMenuBarRegistered(nsMenuBar *aMenuBar,
+                                               bool aSuccess)
+{
+    // Don't assume that GDBus cancellation is reliable (ie, |aMenuBar| might
+    // have already been deleted (see https://launchpad.net/bugs/953562)
+    GCancellable *cancellable = nullptr;
+    if (!mMenuBarRegistrationCancellables.Get(aMenuBar, &cancellable)) {
+        return;
+    }
+
+    g_object_unref(cancellable);
+    mMenuBarRegistrationCancellables.Remove(aMenuBar);
+
+    if (!aSuccess) {
+        aMenuBar->Deactivate();
+    }
+}
+
+/* static */ void
+nsNativeMenuService::PrefChangedCallback(const char *aPref,
+                                         void *aClosure)
+{
+    nsNativeMenuService::GetSingleton()->PrefChanged();
+}
+
+void
+nsNativeMenuService::PrefChanged()
+{
+    if (!mDbusProxy) {
+        SetOnline(false);
+        return;
+    }
+
+    OnNameOwnerChanged();
+}
+
+NS_IMETHODIMP
+nsNativeMenuService::CreateNativeMenuBar(nsIWidget *aParent,
+                                         mozilla::dom::Element *aMenuBarNode)
+{
+    NS_ENSURE_ARG(aParent);
+    NS_ENSURE_ARG(aMenuBarNode);
+
+    if (aMenuBarNode->AttrValueIs(kNameSpaceID_None,
+                                  nsGkAtoms::_moz_menubarkeeplocal,
+                                  nsGkAtoms::_true,
+                                  eCaseMatters)) {
+        return NS_OK;
+    }
+
+    UniquePtr<nsMenuBar> menubar(nsMenuBar::Create(aParent, aMenuBarNode));
+    if (!menubar) {
+        NS_WARNING("Failed to create menubar");
+        return NS_ERROR_FAILURE;
+    }
+
+    // Unity forgets our window if it is unmapped by the application, which
+    // happens with some extensions that add "minimize to tray" type
+    // functionality. We hook on to the MapNotify event to re-register our menu
+    // with Unity
+    g_signal_connect(G_OBJECT(menubar->TopLevelWindow()),
+                     "map-event", G_CALLBACK(map_event_cb),
+                     menubar.get());
+
+    mMenuBars.AppendElement(menubar.get());
+    RegisterNativeMenuBar(menubar.get());
+
+    static_cast<nsWindow *>(aParent)->SetMenuBar(std::move(menubar));
+
+    return NS_OK;
+}
+
+/* static */ already_AddRefed<nsNativeMenuService>
+nsNativeMenuService::GetInstanceForServiceManager()
+{
+    RefPtr<nsNativeMenuService> service(sService);
+
+    if (service) {
+        return service.forget();
+    }
+
+    service = new nsNativeMenuService();
+
+    if (NS_FAILED(service->Init())) {
+        return nullptr;
+    }
+
+    sService = service.get();
+    return service.forget();
+}
+
+/* static */ nsNativeMenuService*
+nsNativeMenuService::GetSingleton()
+{
+    EnsureInitialized();
+    return sService;
+}
+
+void
+nsNativeMenuService::NotifyNativeMenuBarDestroyed(nsMenuBar *aMenuBar)
+{
+    g_signal_handlers_disconnect_by_func(aMenuBar->TopLevelWindow(),
+                                         FuncToGpointer(map_event_cb),
+                                         aMenuBar);
+
+    mMenuBars.RemoveElement(aMenuBar);
+
+    GCancellable *cancellable = nullptr;
+    if (mMenuBarRegistrationCancellables.Get(aMenuBar, &cancellable)) {
+        mMenuBarRegistrationCancellables.Remove(aMenuBar);
+        g_cancellable_cancel(cancellable);
+        g_object_unref(cancellable);
+    }
+}
--- /dev/null
+++ b/widget/gtk/nsNativeMenuService.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=4:
+ */
+/* 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/. */
+
+#ifndef __nsNativeMenuService_h__
+#define __nsNativeMenuService_h__
+
+#include "mozilla/Attributes.h"
+#include "nsCOMPtr.h"
+#include "nsTHashMap.h"
+#include "nsINativeMenuService.h"
+#include "nsTArray.h"
+
+#include <gdk/gdk.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+class nsMenuBar;
+
+/*
+ * The main native menu service singleton.
+ * NativeMenuSupport::CreateNativeMenuBar calls in to this when a new top level
+ * window is created.
+ *
+ * Menubars are owned by their nsWindow. This service holds a weak reference to
+ * each menubar for the purpose of re-registering them with the shell if it
+ * needs to. The menubar is responsible for notifying the service when the last
+ * reference to it is dropped.
+ */
+class nsNativeMenuService final : public nsINativeMenuService
+{
+public:
+    NS_DECL_ISUPPORTS
+
+    NS_IMETHOD CreateNativeMenuBar(nsIWidget* aParent, mozilla::dom::Element* aMenuBarNode) override;
+
+    // Returns the singleton addref'd for the service manager
+    static already_AddRefed<nsNativeMenuService> GetInstanceForServiceManager();
+
+    // Returns the singleton without increasing the reference count
+    static nsNativeMenuService* GetSingleton();
+
+    // Called by a menubar when it is deleted
+    void NotifyNativeMenuBarDestroyed(nsMenuBar *aMenuBar);
+
+private:
+    nsNativeMenuService();
+    ~nsNativeMenuService();
+    nsresult Init();
+
+    static void EnsureInitialized();
+    void SetOnline(bool aOnline);
+    void RegisterNativeMenuBar(nsMenuBar *aMenuBar);
+    static void name_owner_changed_cb(GObject *gobject,
+                                      GParamSpec *pspec,
+                                      gpointer user_data);
+    static void proxy_created_cb(GObject *source_object,
+                                 GAsyncResult *res,
+                                 gpointer user_data);
+    static void register_native_menubar_cb(GObject *source_object,
+                                           GAsyncResult *res,
+                                           gpointer user_data);
+    static gboolean map_event_cb(GtkWidget *widget, GdkEvent *event,
+                                 gpointer user_data);
+    void OnNameOwnerChanged();
+    void OnProxyCreated(GDBusProxy *aProxy);
+    void OnNativeMenuBarRegistered(nsMenuBar *aMenuBar,
+                                   bool aSuccess);
+    static void PrefChangedCallback(const char *aPref, void *aClosure);
+    void PrefChanged();
+
+    GCancellable *mCreateProxyCancellable;
+    GDBusProxy *mDbusProxy;
+    bool mOnline;
+    nsTArray<nsMenuBar *> mMenuBars;
+    nsTHashMap<nsPtrHashKey<nsMenuBar>, GCancellable*> mMenuBarRegistrationCancellables;
+
+    static bool sShutdown;
+    static nsNativeMenuService *sService;
+};
+
+#endif /* __nsNativeMenuService_h__ */
--- a/widget/gtk/nsWindow.cpp
+++ b/widget/gtk/nsWindow.cpp
@@ -7060,6 +7060,10 @@ void nsWindow::HideWindowChrome(bool aSh
   SetWindowDecoration(aShouldHide ? BorderStyle::None : mBorderStyle);
 }
 
+void nsWindow::SetMenuBar(UniquePtr<nsMenuBar> aMenuBar) {
+  mMenuBar = std::move(aMenuBar);
+}
+
 bool nsWindow::CheckForRollup(gdouble aMouseX, gdouble aMouseY, bool aIsWheel,
                               bool aAlwaysRollup) {
   LOG("nsWindow::CheckForRollup() aAlwaysRollup %d", aAlwaysRollup);
--- a/widget/gtk/nsWindow.h
+++ b/widget/gtk/nsWindow.h
@@ -26,6 +26,8 @@
 #include "nsRefPtrHashtable.h"
 #include "IMContextWrapper.h"
 
+#include "nsMenuBar.h"
+
 #ifdef ACCESSIBILITY
 #  include "mozilla/a11y/LocalAccessible.h"
 #endif
@@ -172,6 +174,8 @@ class nsWindow final : public nsBaseWidg
   nsresult MakeFullScreen(bool aFullScreen) override;
   void HideWindowChrome(bool aShouldHide) override;
 
+  void SetMenuBar(mozilla::UniquePtr<nsMenuBar> aMenuBar);
+
   /**
    * GetLastUserInputTime returns a timestamp for the most recent user input
    * event.  This is intended for pointer grab requests (including drags).
@@ -824,6 +828,8 @@ class nsWindow final : public nsBaseWidg
 
   static bool sTransparentMainWindow;
 
+  mozilla::UniquePtr<nsMenuBar> mMenuBar;
+
 #ifdef ACCESSIBILITY
   RefPtr<mozilla::a11y::LocalAccessible> mRootAccessible;
 
--- /dev/null
+++ b/xpcom/ds/NativeMenuAtoms.py
@@ -0,0 +1,9 @@
+from Atom import Atom
+
+NATIVE_MENU_ATOMS = [
+    Atom("menuitem_with_favicon", "menuitem-with-favicon"),
+    Atom("_moz_menubarkeeplocal", "_moz-menubarkeeplocal"),
+    Atom("_moz_nativemenupopupstate", "_moz-nativemenupopupstate"),
+    Atom("openedwithkey", "openedwithkey"),
+    Atom("shellshowingmenubar", "shellshowingmenubar"),
+]
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -7,6 +7,7 @@
     PseudoElementAtom,
 )
 from HTMLAtoms import HTML_PARSER_ATOMS
+from NativeMenuAtoms import NATIVE_MENU_ATOMS
 
 # Static atom definitions, used to generate nsGkAtomList.h.
 #
@@ -2529,7 +2530,7 @@ STATIC_ATOMS = [
     InheritingAnonBoxAtom("AnonBox_mozSVGForeignContent", ":-moz-svg-foreign-content"),
     InheritingAnonBoxAtom("AnonBox_mozSVGText", ":-moz-svg-text"),
     # END ATOMS
-] + HTML_PARSER_ATOMS
+] + HTML_PARSER_ATOMS + NATIVE_MENU_ATOMS
 # fmt: on
 
 
--- a/widget/gtk/components.conf
+++ b/widget/gtk/components.conf
@@ -76,6 +76,14 @@ Classes = [
         'headers': ['/widget/gtk/nsUserIdleServiceGTK.h'],
         'constructor': 'nsUserIdleServiceGTK::GetInstance',
     },
+    {
+        'cid': '{0b3fe5aa-bc72-4303-85ae-76365df1251d}',
+        'contract_ids': ['@mozilla.org/widget/nativemenuservice;1'],
+        'singleton': True,
+        'type': 'nsNativeMenuService',
+        'constructor': 'nsNativeMenuService::GetInstanceForServiceManager',
+        'headers': ['/widget/gtk/nsNativeMenuService.h'],
+    },
 ]
 
 if defined('NS_PRINTING'):
--- a/xpfe/appshell/AppWindow.cpp
+++ b/xpfe/appshell/AppWindow.cpp
@@ -80,7 +80,7 @@
 
 #include "mozilla/dom/DocumentL10n.h"
 
-#ifdef XP_MACOSX
+#if defined(XP_MACOSX) || defined(MOZ_WIDGET_GTK)
 #  include "mozilla/widget/NativeMenuSupport.h"
 #  define USE_NATIVE_MENUS
 #endif
--- a/widget/gtk/NativeMenuSupport.cpp
+++ b/widget/gtk/NativeMenuSupport.cpp
@@ -7,6 +7,8 @@
 
 #include "MainThreadUtils.h"
 #include "NativeMenuGtk.h"
+#include "nsINativeMenuService.h"
+#include "nsServiceManagerUtils.h"
 
 namespace mozilla::widget {
 
@@ -14,7 +16,14 @@ void NativeMenuSupport::CreateNativeMenu
                                             dom::Element* aMenuBarElement) {
   MOZ_RELEASE_ASSERT(NS_IsMainThread(),
                      "Attempting to create native menu bar on wrong thread!");
-  // TODO
+
+  nsCOMPtr<nsINativeMenuService> nms =
+      do_GetService("@mozilla.org/widget/nativemenuservice;1");
+  if (!nms) {
+    return;
+  }
+
+  nms->CreateNativeMenuBar(aParent, aMenuBarElement);
 }
 
 already_AddRefed<NativeMenu> NativeMenuSupport::CreateNativeContextMenu(
--- /dev/null
+++ b/widget/gtk/NativeMenuSupport.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef mozilla_widget_NativeMenuSupport_h
+#define mozilla_widget_NativeMenuSupport_h
+
+class nsIWidget;
+
+namespace mozilla {
+
+namespace dom {
+class Element;
+}
+
+namespace widget {
+
+class NativeMenuSupport final {
+public:
+    // Given a top-level window widget and a menu bar DOM node, sets up native
+    // menus. Once created, native menus are controlled via the DOM, including
+    // destruction.
+    static void CreateNativeMenuBar(nsIWidget* aParent,
+                                    dom::Element* aMenuBarElement);
+};
+
+}  // namespace widget
+}  // namespace mozilla
+
+#endif  // mozilla_widget_NativeMenuSupport_h
--- a/widget/moz.build
+++ b/widget/moz.build
@@ -157,6 +157,11 @@ EXPORTS += [
     "PuppetWidget.h",
 ]
 
+if toolkit == "gtk":
+    EXPORTS += [
+        "nsINativeMenuService.h",
+    ]
+
 EXPORTS.mozilla += [
     "BasicEvents.h",
     "ColorScheme.h",
--- /dev/null
+++ b/widget/nsINativeMenuService.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsINativeMenuService_h_
+#define nsINativeMenuService_h_
+
+#include "nsISupports.h"
+
+class nsIWidget;
+class nsIContent;
+namespace mozilla {
+namespace dom {
+class Element;
+}
+}  // namespace mozilla
+
+// {90DF88F9-F084-4EF3-829A-49496E636DED}
+#define NS_INATIVEMENUSERVICE_IID                    \
+  {                                                  \
+    0x90DF88F9, 0xF084, 0x4EF3, {                    \
+      0x82, 0x9A, 0x49, 0x49, 0x6E, 0x63, 0x6D, 0xED \
+    }                                                \
+  }
+
+class nsINativeMenuService : public nsISupports {
+ public:
+  NS_DECLARE_STATIC_IID_ACCESSOR(NS_INATIVEMENUSERVICE_IID)
+  // Given a top-level window widget and a menu bar DOM node, sets up native
+  // menus. Once created, native menus are controlled via the DOM, including
+  // destruction.
+  NS_IMETHOD CreateNativeMenuBar(nsIWidget* aParent,
+                                 mozilla::dom::Element* aMenuBarNode) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsINativeMenuService, NS_INATIVEMENUSERVICE_IID)
+
+#endif  // nsINativeMenuService_h_
--- a/widget/nsWidgetsCID.h
+++ b/widget/nsWidgetsCID.h
@@ -66,6 +66,14 @@
 // Menus
 //-----------------------------------------------------------
 
+// {0B3FE5AA-BC72-4303-85AE-76365DF1251D}
+#define NS_NATIVEMENUSERVICE_CID                     \
+  {                                                  \
+    0x0B3FE5AA, 0xBC72, 0x4303, {                    \
+      0x85, 0xAE, 0x76, 0x36, 0x5D, 0xF1, 0x25, 0x1D \
+    }                                                \
+  }
+
 // {F6CD4F21-53AF-11d2-8DC4-00609703C14E}
 #define NS_POPUPMENU_CID                           \
   {                                                \
