/* 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; } 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); } }