Files
tubestation/widget/gtk/nsNativeMenuService.cpp

479 lines
14 KiB
C++

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