Mypal/widget/gtk/nsNativeMenuService.cpp

486 lines
15 KiB
C++

/* 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 <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;
static const nsTArray<nsMenuBar* >::index_type NoIndex = nsTArray<nsMenuBar* >::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<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_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<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,
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<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(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);
}
}