From d18faf81938c947f1d02feab2c394b8135f88d8f Mon Sep 17 00:00:00 2001 From: Lootyhoof Date: Sat, 6 Jun 2020 00:37:29 +0100 Subject: Issue MoonchildProductions/UXP#1578 - Add global menubar support for GTK --- widget/gtk/nsNativeMenuService.cpp | 485 +++++++++++++++++++++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 widget/gtk/nsNativeMenuService.cpp (limited to 'widget/gtk/nsNativeMenuService.cpp') diff --git a/widget/gtk/nsNativeMenuService.cpp b/widget/gtk/nsNativeMenuService.cpp new file mode 100644 index 0000000000..eabe77ca80 --- /dev/null +++ b/widget/gtk/nsNativeMenuService.cpp @@ -0,0 +1,485 @@ +/* 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/Move.h" +#include "mozilla/Preferences.h" +#include "mozilla/UniquePtr.h" +#include "nsAutoPtr.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 "nsNativeMenuAtoms.h" +#include "nsNativeMenuDocListener.h" + +#include +#include +#include + +#include "nsNativeMenuService.h" + +using namespace mozilla; + +nsNativeMenuService* nsNativeMenuService::sService = nullptr; + +extern PangoLayout* gPangoLayout; +extern nsNativeMenuDocListenerTArray* gPendingListeners; + +static const nsTArray::index_type NoIndex = nsTArray::NoIndex; + +#if not GLIB_CHECK_VERSION(2,26,0) +enum GBusType { + G_BUS_TYPE_STARTER = -1, + G_BUS_TYPE_NONE = 0, + G_BUS_TYPE_SYSTEM = 1, + G_BUS_TYPE_SESSION = 2 +}; + +enum GDBusProxyFlags { + G_DBUS_PROXY_FLAGS_NONE = 0, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES = 1 << 0, + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS = 1 << 1, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START = 1 << 2, + G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES = 1 << 3 +}; + +enum GDBusCallFlags { + G_DBUS_CALL_FLAGS_NONE = 0, + G_DBUS_CALL_FLAGS_NO_AUTO_START = 1 << 0 +}; + +typedef _GDBusInterfaceInfo GDBusInterfaceInfo; +typedef _GDBusProxy GDBusProxy; +typedef _GVariant GVariant; +#endif + +#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_global_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_global_menubar"); + + mCreateProxyCancellable = g_cancellable_new(); + + g_dbus_proxy_new_for_bus(G_BUS_TYPE_SESSION, + static_cast( + 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 service = + do_GetService("@mozilla.org/widget/nativemenuservice;1"); +} + +void +nsNativeMenuService::SetOnline(bool aOnline) { + if (!Preferences::GetBool("ui.use_global_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(); + nsAdoptingCString path = aMenuBar->ObjectPath(); + if (xid == 0 || path.IsEmpty()) { + NS_WARNING("Menubar has invalid XID or object path"); + return; + } + + GCancellable* cancellable = g_cancellable_new(); + mMenuBarRegistrationCancellables.Put(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(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(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, + nsIContent* aMenuBarNode) { + NS_ENSURE_ARG(aParent); + NS_ENSURE_ARG(aMenuBarNode); + + if (aMenuBarNode->AttrValueIs(kNameSpaceID_None, + nsNativeMenuAtoms::_moz_menubarkeeplocal, + nsGkAtoms::_true, + eCaseMatters)) { + return NS_OK; + } + + UniquePtr 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(aParent)->SetMenuBar(Move(menubar)); + + return NS_OK; +} + +/* static */ already_AddRefed +nsNativeMenuService::GetInstanceForServiceManager() { + RefPtr 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); + } +} -- cgit v1.2.3