diff --git a/libportal/globalshortcuts.c b/libportal/globalshortcuts.c new file mode 100644 index 00000000..1bc04f13 --- /dev/null +++ b/libportal/globalshortcuts.c @@ -0,0 +1,925 @@ +/* + * Copyright (C) 2022, Red Hat, Inc. + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, version 3.0 of the + * License. + * + * This file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see . + * + * SPDX-License-Identifier: LGPL-3.0-only + */ + +#include "config.h" + +#include +#include +#include + +#include "globalshortcuts.h" +#include "portal-private.h" +#include "session-private.h" + +/** + * XdpGlobalShortcutsSession + * + * A representation of a long-lived global shortcuts portal interaction. + * + * The [class@GlobalShortcutsSession] object is used to represent portal + * interactions with the global shortcuts desktop portal that extend over + * multiple portal calls. Usually a caller creates a global shortcuts session, + * binds shortcuts, and then starts listening to activations. + * + * To list current assignments after [method@GlobalShortcutsSession.bind_shortcuts] returns, + * call [method@GlobalShortcutsSession.list_shortcuts]. + * + * The [class@GlobalShortcutsSession] wraps a [class@Session] object. + */ + +enum { + SIGNAL_CLOSED, + SIGNAL_ACTIVATED, + SIGNAL_DEACTIVATED, + SIGNAL_SHORTCUTS_CHANGED, + SIGNAL_LAST_SIGNAL, +}; + +static guint signals[SIGNAL_LAST_SIGNAL]; + +struct _XdpGlobalShortcutsSession +{ + GObject parent_instance; + XdpSession *parent_session; /* strong ref */ + guint signal_ids[SIGNAL_LAST_SIGNAL]; +}; + +G_DEFINE_TYPE (XdpGlobalShortcutsSession, xdp_global_shortcuts_session, G_TYPE_OBJECT) + +static gboolean +_xdp_global_shortcuts_session_is_valid (XdpGlobalShortcutsSession *session) +{ + return XDP_IS_GLOBAL_SHORTCUTS_SESSION (session) && session->parent_session != NULL; +} + +static void +parent_session_destroy (gpointer data, GObject *old_session) +{ + XdpGlobalShortcutsSession *session = XDP_GLOBAL_SHORTCUTS_SESSION (data); + + g_critical ("XdpSession destroyed before XdpGlobalShortcutsSesssion, you lost count of your session refs"); + + session->parent_session = NULL; +} + +static void +xdp_global_shortcuts_session_finalize (GObject *object) +{ + XdpGlobalShortcutsSession *session = XDP_GLOBAL_SHORTCUTS_SESSION (object); + XdpSession *parent_session = session->parent_session; + + if (parent_session == NULL) + { + g_critical ("XdpSession destroyed before XdpGlobalShortcutsSesssion, you lost count of your session refs"); + } + else + { + for (guint i = 0; i < SIGNAL_LAST_SIGNAL; i++) + { + guint signal_id = session->signal_ids[i]; + if (signal_id > 0) + g_dbus_connection_signal_unsubscribe (parent_session->portal->bus, signal_id); + } + + g_object_weak_unref (G_OBJECT (parent_session), parent_session_destroy, session); + + g_clear_pointer (&session->parent_session, g_object_unref); + } + + G_OBJECT_CLASS (xdp_global_shortcuts_session_parent_class)->finalize (object); +} + +static void +xdp_global_shortcuts_session_class_init (XdpGlobalShortcutsSessionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = xdp_global_shortcuts_session_finalize; + + /** + * XdpGlobalShortcutsSession::shortcuts-changed: + * @session: the [class@GlobalShortcutsSession] + * @options: a GVariant with the signal options + * + * Emitted when an GlobalShortcuts session's shortcuts have changed. This + * signal is emitted after new shortcuts have already become effective. + */ + signals[SIGNAL_SHORTCUTS_CHANGED] = + g_signal_new ("shortcuts-changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 1, + G_TYPE_VARIANT); + /** + * XdpGlobalShortcutsSession::activated: + * @session: the [class@GlobalShortcutsSession] + * @name: shortcut ID + * @timestamp: measured since epoch + * + * Emitted when a GlobalShortcuts shortcut of this session was activated. + */ + signals[SIGNAL_ACTIVATED] = + g_signal_new ("activated", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + G_TYPE_STRING, + G_TYPE_UINT); + /** + * XdpGlobalShortcutsSession::deactivated: + * @session: the [class@GlobalShortcutsSession] + * @name: shortcut ID + * @timestamp: measured since epoch + * + * Emitted when a GlobalShortcuts shortcut of this session was deactivated. + */ + signals[SIGNAL_DEACTIVATED] = + g_signal_new ("deactivated", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + G_TYPE_STRING, + G_TYPE_UINT); +} + +static void +xdp_global_shortcuts_session_init (XdpGlobalShortcutsSession *session) +{ + session->parent_session = NULL; + for (guint i = 0; i < SIGNAL_LAST_SIGNAL; i++) + session->signal_ids[i] = 0; +} + +/* A request-based method call */ +typedef struct { + XdpPortal *portal; + char *session_path; /* object path for session */ + GTask *task; + guint signal_id; /* Request::Response signal */ + char *request_path; /* object path for request */ + guint cancelled_id; /* signal id for cancelled gobject signal */ + + /* GetZones only */ + XdpGlobalShortcutsSession *session; + +} Call; + +static void create_session (Call *call); + +static void +call_free (Call *call) +{ + /* Generic */ + if (call->signal_id) + g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id); + + if (call->cancelled_id) + g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id); + + g_free (call->request_path); + + g_clear_object (&call->portal); + g_clear_object (&call->task); + g_clear_object (&call->session); + + g_free (call->session_path); + + g_free (call); +} + +static void +call_returned (GObject *object, + GAsyncResult *result, + gpointer data) +{ + Call *call = data; + GError *error = NULL; + g_autoptr(GVariant) ret; + + ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); + if (error) + { + if (call->cancelled_id) + { + g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id); + call->cancelled_id = 0; + } + g_task_return_error (call->task, error); + call_free (call); + } +} + +static gboolean +handle_matches_session (XdpGlobalShortcutsSession *session, const char *id) +{ + const char *sid = session->parent_session->id; + + return g_str_equal (sid, id); +} + + +static void +prep_call (Call *call, GDBusSignalCallback callback, GVariantBuilder *options, void *userdata) +{ + g_autofree char *token = NULL; + + token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT)); + call->request_path = g_strconcat (REQUEST_PATH_PREFIX, call->portal->sender, "/", token, NULL); + call->signal_id = g_dbus_connection_signal_subscribe (call->portal->bus, + PORTAL_BUS_NAME, + REQUEST_INTERFACE, + "Response", + call->request_path, + NULL, + G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, + callback, + call, + userdata); + + g_variant_builder_init (options, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (options, "{sv}", "handle_token", g_variant_new_string (token)); +} + +static void +shortcuts_changed_emit_signal (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + XdpGlobalShortcutsSession *session = XDP_GLOBAL_SHORTCUTS_SESSION (data); + GVariantBuilder options; + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); +// g_variant_builder_add (&options, "{sv}", "zone_set", g_variant_new_uint32 (session->zone_set - 1)); + + g_signal_emit (session, signals[SIGNAL_SHORTCUTS_CHANGED], 0, g_variant_new ("a{sv}", &options)); +} + +static void +shortcuts_changed (GDBusConnection *bus, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer data) +{ + XdpGlobalShortcutsSession *session = XDP_GLOBAL_SHORTCUTS_SESSION (data); + XdpPortal *portal = session->parent_session->portal; + g_autoptr(GVariant) options = NULL; + const char *handle = NULL; + Call *call; + + g_variant_get(parameters, "(o@a{sv})", &handle, &options); + + if (!handle_matches_session (session, handle)) + return; + + /* Zones have changed, but let's fetch the new zones before we notify the + * caller so they're already available by the time they get notified */ + call = g_new0 (Call, 1); + call->portal = g_object_ref (portal); + call->task = g_task_new (portal, NULL, shortcuts_changed_emit_signal, session); + call->session = g_object_ref (session); +} + +static void +activated (GDBusConnection *bus, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer data) +{ + XdpGlobalShortcutsSession *session = XDP_GLOBAL_SHORTCUTS_SESSION (data); + g_autoptr(GVariant) options = NULL; + guint32 activation_id = 0; + const char *handle = NULL; + + g_variant_get (parameters, "(o@a{sv})", &handle, &options); + + /* FIXME: we should remove the activation_id from options, but ... meh? */ + if (!g_variant_lookup (options, "activation_id", "u", &activation_id)) + g_warning ("Portal bug: activation_id missing from Activated signal"); + + if (!handle_matches_session (session, handle)) + return; + + g_signal_emit (session, signals[SIGNAL_ACTIVATED], 0, activation_id, options); +} + +static void +deactivated (GDBusConnection *bus, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer data) +{ + XdpGlobalShortcutsSession *session = XDP_GLOBAL_SHORTCUTS_SESSION (data); + g_autoptr(GVariant) options = NULL; + guint32 activation_id = 0; + const char *handle = NULL; + + g_variant_get(parameters, "(o@a{sv})", &handle, &options); + + /* FIXME: we should remove the activation_id from options, but ... meh? */ + if (!g_variant_lookup (options, "activation_id", "u", &activation_id)) + g_warning ("Portal bug: activation_id missing from Deactivated signal"); + + if (!handle_matches_session (session, handle)) + return; + + g_signal_emit (session, signals[SIGNAL_DEACTIVATED], 0, activation_id, options); +} + +static XdpGlobalShortcutsSession * +_xdp_global_shortcuts_session_new (XdpPortal *portal, const char *session_path) +{ + g_autoptr(XdpSession) parent_session = _xdp_session_new (portal, session_path, XDP_SESSION_GLOBAL_SHORTCUTS); + g_autoptr(XdpGlobalShortcutsSession) session = g_object_new (XDP_TYPE_GLOBAL_SHORTCUTS_SESSION, NULL); + + //parent_session->input_capture_session = session; /* weak ref */ + g_object_weak_ref (G_OBJECT (parent_session), parent_session_destroy, session); + session->parent_session = g_object_ref(parent_session); /* strong ref */ + + return g_object_ref(session); +} + + +void +xdp_global_shortcuts_session_close (XdpGlobalShortcutsSession *session) +{ + xdp_session_close (session->parent_session); +} + +static void +bind_shortcuts_done (GDBusConnection *bus, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer data) +{ + Call *call = data; + guint32 response; + g_autoptr(GVariant) ret = NULL; + + g_variant_get (parameters, "(u@a{sv})", &response, &ret); + + if (response != 0 && call->cancelled_id) + { + g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id); + call->cancelled_id = 0; + } + + if (response == 0) + { + XdpGlobalShortcutsSession *session = call->session; + + g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id); + call->signal_id = 0; + + if (session == NULL) + { + session = _xdp_global_shortcuts_session_new (call->portal, call->session_path); + session->signal_ids[SIGNAL_SHORTCUTS_CHANGED] = + g_dbus_connection_signal_subscribe (bus, + PORTAL_BUS_NAME, + "org.freedesktop.portal.GlobalShortcuts", + "ShortcutsChanged", + PORTAL_OBJECT_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, + shortcuts_changed, + session, + NULL); + + session->signal_ids[SIGNAL_ACTIVATED] = + g_dbus_connection_signal_subscribe (bus, + PORTAL_BUS_NAME, + "org.freedesktop.portal.GlobalShortcuts", + "Activated", + PORTAL_OBJECT_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, + activated, + session, + NULL); + + session->signal_ids[SIGNAL_DEACTIVATED] = + g_dbus_connection_signal_subscribe (bus, + PORTAL_BUS_NAME, + "org.freedesktop.portal.GlobalShortcuts", + "Deactivated", + PORTAL_OBJECT_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, + deactivated, + session, + NULL); + } + g_task_return_pointer (call->task, ret, g_object_unref); + } + + if (response == 1) + g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "GlobalShortcuts BindShortcuts() canceled"); + else if (response == 2) + g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "GlobalShortcuts BindShortcuts() failed"); + + if (response != 0) + call_free (call); +} + +static void +session_created (GDBusConnection *bus, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer data) +{ + Call *call = data; + guint32 response; + g_autoptr(GVariant) ret = NULL; + + g_variant_get (parameters, "(u@a{sv})", &response, &ret); + + if (response != 0 && call->cancelled_id) + { + g_signal_handler_disconnect (g_task_get_cancellable (call->task), call->cancelled_id); + call->cancelled_id = 0; + } + + if (response == 0) + { + g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id); + call->signal_id = 0; + + if (!g_variant_lookup (ret, "session_handle", "s", &call->session_path)) + { + g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "CreateSession failed to return a session handle"); + response = 2; + } + else + { + call->session = _xdp_global_shortcuts_session_new (call->portal, call->session_path); + g_task_return_pointer (call->task, call->session, g_object_unref); + } + } + else if (response == 1) + g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "CreateSession canceled"); + else if (response == 2) + g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "CreateSession failed"); + + if (response != 0) + call_free (call); +} + +static void +call_cancelled_cb (GCancellable *cancellable, + gpointer data) +{ + Call *call = data; + + g_dbus_connection_call (call->portal->bus, + PORTAL_BUS_NAME, + call->request_path, + REQUEST_INTERFACE, + "Close", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, NULL, NULL); +} + +static void +create_session (Call *call) +{ + GVariantBuilder options; + g_autofree char *session_token = NULL; + GCancellable *cancellable; + + cancellable = g_task_get_cancellable (call->task); + if (cancellable) + call->cancelled_id = g_signal_connect (cancellable, "cancelled", G_CALLBACK (call_cancelled_cb), call); + + session_token = g_strdup_printf ("portal%d", g_random_int_range (0, G_MAXINT)); + + prep_call (call, session_created, &options, NULL); + g_variant_builder_add (&options, "{sv}", "session_handle_token", g_variant_new_string (session_token)); + + g_dbus_connection_call (call->portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.portal.GlobalShortcuts", + "CreateSession", + g_variant_new ("(a{sv})", &options), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + cancellable, + call_returned, + call); +} + +/** + * xdp_portal_create_global_shortcuts_session: + * @portal: a [class@Portal] + * @cancellable: (nullable): optional [class@Gio.Cancellable] + * @callback: (scope async): a callback to call when the request is done + * @data: (closure): data to pass to @callback + * + * Creates a session for global shortcuts + * + * When the request is done, @callback will be called. You can then + * call [method@Portal.create_global_shortcuts_session_finish] to get the results. + */ +void +xdp_portal_create_global_shortcuts_session (XdpPortal *portal, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data) +{ + Call *call; + + g_return_if_fail (XDP_IS_PORTAL (portal)); + + call = g_new0 (Call, 1); + call->portal = g_object_ref (portal); + call->task = g_task_new (portal, cancellable, callback, data); + + create_session (call); +} + +/** + * xdp_portal_create_global_shortcuts_session_finish: + * @portal: a [class@Portal] + * @result: a [iface@Gio.AsyncResult] + * @error: return location for an error + * + * Finishes the GlobalShortcuts CreateSession request, and returns a + * [class@GlobalShortcutsSession]. To get to the [class@Session] within use + * xdp_global_shortcuts_session_get_session(). + * + * Returns: (transfer full): a [class@GlobalShortcutsSession] + */ +XdpGlobalShortcutsSession * +xdp_portal_create_global_shortcuts_session_finish (XdpPortal *portal, + GAsyncResult *result, + GError **error) +{ + XdpGlobalShortcutsSession *session; + + g_return_val_if_fail (XDP_IS_PORTAL (portal), NULL); + g_return_val_if_fail (g_task_is_valid (result, portal), NULL); + + session = g_task_propagate_pointer (G_TASK (result), error); + + if (session) + return session; + else + return NULL; +} + +/** + * xdp_global_shortcuts_session_get_session: + * @session: a [class@XdpGlobalShortcutsSession] + * + * Return the [class@XdpSession] for this GlobalShortcuts session. + * + * Returns: (transfer none): a [class@Session] object + */ +XdpSession * +xdp_global_shortcuts_session_get_session (XdpGlobalShortcutsSession *session) +{ + return session->parent_session; +} + +/** + * xdp_global_shortcuts_session_connect_to_eis: + * @session: a [class@InputCaptureSession] + * @error: return location for a #GError pointer + * + * Connect this session to an EIS implementation and return the fd. + * This fd can be passed into ei_setup_backend_fd(). See the libei + * documentation for details. + * + * This is a sync DBus invocation. + * + * Returns: a socket to the EIS implementation for this input capture + * session or a negative errno on failure. + */ +int +xdp_global_shortcuts_session_connect_to_eis (XdpGlobalShortcutsSession *session, + GError **error) +{ + GVariantBuilder options; + g_autoptr(GVariant) ret = NULL; + g_autoptr(GUnixFDList) fd_list = NULL; + int fd_out; + XdpPortal *portal; + XdpSession *parent_session = session->parent_session; + + if (!_xdp_global_shortcuts_session_is_valid (session)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, "Session is not a GlobalShortcuts session"); + return -1; + } + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + + portal = parent_session->portal; + ret = g_dbus_connection_call_with_unix_fd_list_sync (portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.portal.InputCapture", + "ConnectToEIS", + g_variant_new ("(oa{sv})", + parent_session->id, + &options), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &fd_list, + NULL, + error); + + if (!ret) + return -1; + + g_variant_get (ret, "(h)", &fd_out); + + return g_unix_fd_list_get (fd_list, fd_out, NULL); +} + +static void +free_barrier_list (GList *list) +{ + g_list_free_full (list, g_object_unref); +} + +static void +list_shortcuts_done (GDBusConnection *bus, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer data) +{ + Call *call = data; + guint32 response; + g_autoptr(GVariant) ret = NULL; + GList *failed_list = NULL; + + g_variant_get (parameters, "(u@a{sv})", &response, &ret); + + g_task_return_pointer (call->task, failed_list, (GDestroyNotify)free_barrier_list); +} + + +static void +list_shortcuts (Call *call) +{ + GVariantBuilder options; + prep_call (call, list_shortcuts_done, &options, NULL); + + g_dbus_connection_call (call->portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.portal.GlobalShortcuts", + "ListShortcuts", + g_variant_new ("(oa{sv})", + call->session->parent_session->id, + &options), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + g_task_get_cancellable (call->task), + call_returned, + call); +} + +/** + * xdp_global_shortcuts_session_bind_shortcuts: + * @session: a [class@GlobalShortcutsSession] + * @shortcuts: GArray + * + * Bind shortcuts and list triggers. + */ +void xdp_global_shortcuts_session_bind_shortcuts(XdpGlobalShortcutsSession *session, + // Contains XdpGlobalShortcut elements + GArray *shortcuts, + char *parent_window, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data) +{ + Call *call; + XdpPortal *portal; + guint i; + GVariantBuilder options; + GVariantBuilder shortcuts_builder; + g_autoptr(GVariantType) vtype; + + g_return_if_fail (_xdp_global_shortcuts_session_is_valid (session)); + g_return_if_fail (shortcuts != NULL); + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + + portal = session->parent_session->portal; + + call = g_new0 (Call, 1); + call->portal = g_object_ref (portal); + call->task = g_task_new (portal, cancellable, callback, data); + + prep_call (call, bind_shortcuts_done, &options, NULL); + + vtype = g_variant_type_new ("a(sa{sv})"); + + g_variant_builder_init (&shortcuts_builder, vtype); + + for (i = 0; i < shortcuts->len; i++) { + GVariantDict dict; + g_auto (GStrv) combos = NULL; + struct XdpGlobalShortcut shortcut = ((struct XdpGlobalShortcut*)shortcuts->data)[i]; + + g_variant_dict_init (&dict, NULL); + if (shortcut.preferred_trigger) + g_variant_dict_insert (&dict, "preferred_trigger", "s", shortcut.preferred_trigger); + g_variant_dict_insert (&dict, "description", "s", shortcut.description); + + g_variant_builder_add (&shortcuts_builder, "(s@a{sv})", shortcut.name, g_variant_dict_end (&dict)); + } + + if (parent_window == NULL) + parent_window = ""; + + GVariant *v = g_variant_new ("(o@a(sa{sv})s@a{sv})", + session->parent_session->id, + g_variant_builder_end(&shortcuts_builder), + parent_window, + g_variant_builder_end(&options)); + + g_dbus_connection_call (call->portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.portal.GlobalShortcuts", + "BindShortcuts", + v, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + g_task_get_cancellable (call->task), + call_returned, + call); +} + + +/** + * xdp_global_shortcuts_session_bind_shortcuts_finish: + * @session: a [class@GlobalShortcutsSession] + * @result: a [iface@Gio.AsyncResult] + * @error: return location for an error + * + * Finishes the GlobalShortcuts BindShortcuts request. + * + * Returns: (transfer full): an array of [GlobalShortcutAssigned]. + */ +GArray * +xdp_global_shortcuts_session_bind_shortcuts_finish (XdpGlobalShortcutsSession *session, + GAsyncResult *result, + GError **error) +{ + GVariant *r; + g_return_val_if_fail (XDP_IS_GLOBAL_SHORTCUTS_SESSION (session), NULL); + g_return_val_if_fail (g_task_is_valid (result, session->parent_session->portal), NULL); + + r = g_task_propagate_pointer (G_TASK (result), error); + if (r) + { + GVariantIter *items; + if (g_variant_lookup(r, "shortcuts", "a(sa{sv})", &items)) + { + //GVariantIter items; + char *name; + GVariant *item; + struct XdpGlobalShortcutAssigned *shortcuts; + GArray *ret; + guint i = 0; + //g_print("%s", g_variant_print(s, TRUE)); +// g_variant_iter_init(&items, s); + + shortcuts = g_new0(struct XdpGlobalShortcutAssigned, + g_variant_iter_n_children(items)); + + while (g_variant_iter_next(items, "(s@a{sv})", &name, &item)) + { + struct XdpGlobalShortcutAssigned *shortcut = &shortcuts[i]; + shortcut->name = name; + g_print(g_variant_print(item, TRUE)); + g_variant_lookup(item, "trigger_description", "s", &shortcut->trigger_description); + i++; + } + ret = g_array_new_take(shortcuts, + g_variant_iter_n_children(items), + TRUE, + sizeof(struct XdpGlobalShortcutAssigned)); + + return ret; + } + else + { + g_debug("%s", g_variant_print(r, TRUE)); + g_error("GlobalShortcuts::BindShortcuts() did not return \"shortcuts\" key."); + return NULL; + } + } + else + { + return NULL; + } +} + +/** + * xdp_global_shortcuts_session_list_shortcuts: + * @session: a [class@GlobalShortcutsSession] + * + * List currently registered shortcuts and triggers. + */ +void +xdp_global_shortcuts_session_list_shortcuts (XdpGlobalShortcutsSession *session, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data) +{ + Call *call; + XdpPortal *portal; + + g_return_if_fail (_xdp_global_shortcuts_session_is_valid (session)); + + portal = session->parent_session->portal; + + + call = g_new0 (Call, 1); + call->portal = g_object_ref (portal); + call->session = g_object_ref (session); + call->task = g_task_new (session, cancellable, callback, data); + + list_shortcuts (call); +} + +/** + * xdp_global_shortcuts_session_list_shortcuts_finish: + * @session: a [class@GlobalShortcutsSession] + * @result: a [iface@Gio.AsyncResult] + * @error: return location for an error + * + * Finishes the list-shortcuts request, and returns a GList + * with the shortcuts. + * + * Returns: (element-type XdpGlobalShortcutsAssigned) (transfer full): a list of failed pointer barriers + */ + +GList * +xdp_global_shortcuts_session_list_shortcuts_finish (XdpGlobalShortcutsSession *session, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (_xdp_global_shortcuts_session_is_valid (session), NULL); + g_return_val_if_fail (g_task_is_valid (result, session), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + diff --git a/libportal/globalshortcuts.h b/libportal/globalshortcuts.h new file mode 100644 index 00000000..b13ea54d --- /dev/null +++ b/libportal/globalshortcuts.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018, Matthias Clasen + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, version 3.0 of the + * License. + * + * This file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see . + * + * SPDX-License-Identifier: LGPL-3.0-only + */ + +#pragma once + +#include +#include +#include + +G_BEGIN_DECLS + +#define XDP_TYPE_GLOBAL_SHORTCUTS_SESSION (xdp_global_shortcuts_session_get_type ()) + +XDP_PUBLIC +G_DECLARE_FINAL_TYPE (XdpGlobalShortcutsSession, xdp_global_shortcuts_session, XDP, GLOBAL_SHORTCUTS_SESSION, GObject) + + +struct XdpGlobalShortcut { + // Shortcut ID, Owned by caller + char *name; + // Shortcut description, Owned by caller + char *description; + // Shortcut suggested trigger, nullable, Owned by caller + char *preferred_trigger; +}; + +struct XdpGlobalShortcutAssigned { + // Shortcut ID, Owned + char *name; + // Human-readable trigger description, Owned + char *trigger_description; +}; + + +XDP_PUBLIC +void xdp_portal_create_global_shortcuts_session (XdpPortal *portal, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data); + +XDP_PUBLIC +XdpGlobalShortcutsSession * xdp_portal_create_global_shortcuts_session_finish (XdpPortal *portal, + GAsyncResult *result, + GError **error); + +XDP_PUBLIC +XdpSession *xdp_global_shortcuts_session_get_session (XdpGlobalShortcutsSession *session); + +XDP_PUBLIC +void xdp_global_shortcuts_session_close (XdpGlobalShortcutsSession *session); + +XDP_PUBLIC +void xdp_global_shortcuts_session_bind_shortcuts(XdpGlobalShortcutsSession *session, + // Contains XdpGlobalShortcut elements + GArray *shortcuts, char *parent_window, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data); + +XDP_PUBLIC +GArray * xdp_global_shortcuts_session_bind_shortcuts_finish (XdpGlobalShortcutsSession *session, + GAsyncResult *result, + GError **error); + +XDP_PUBLIC +void xdp_global_shortcuts_session_release (XdpGlobalShortcutsSession *session); + + +G_END_DECLS diff --git a/libportal/meson.build b/libportal/meson.build index 4e67f409..065b33e0 100644 --- a/libportal/meson.build +++ b/libportal/meson.build @@ -11,6 +11,7 @@ headers = [ 'dynamic-launcher.h', 'email.h', 'filechooser.h', + 'globalshortcuts.h', 'inhibit.h', 'inputcapture.h', 'inputcapture-zone.h', @@ -47,6 +48,7 @@ src = [ 'dynamic-launcher.c', 'email.c', 'filechooser.c', + 'globalshortcuts.c', 'inhibit.c', 'inputcapture.c', 'inputcapture-zone.c', diff --git a/libportal/portal.h b/libportal/portal.h index 3618b81e..97c25790 100644 --- a/libportal/portal.h +++ b/libportal/portal.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include diff --git a/libportal/session.h b/libportal/session.h index e9f02145..fcb8140c 100644 --- a/libportal/session.h +++ b/libportal/session.h @@ -33,6 +33,7 @@ G_DECLARE_FINAL_TYPE (XdpSession, xdp_session, XDP, SESSION, GObject) * @XDP_SESSION_SCREENCAST: a screencast session. * @XDP_SESSION_REMOTE_DESKTOP: a remote desktop session. * @XDP_SESSION_INPUT_CAPTURE: an input capture session. + * @XDP_SESSION_GLOBAL_SHORTCUTS: a global shortcuts session. * * The type of a session. */ @@ -40,6 +41,7 @@ typedef enum { XDP_SESSION_SCREENCAST, XDP_SESSION_REMOTE_DESKTOP, XDP_SESSION_INPUT_CAPTURE, + XDP_SESSION_GLOBAL_SHORTCUTS, } XdpSessionType; XDP_PUBLIC diff --git a/portal-test/gtk3/portal-test-win.c b/portal-test/gtk3/portal-test-win.c index e95c1c13..22f8a362 100644 --- a/portal-test/gtk3/portal-test-win.c +++ b/portal-test/gtk3/portal-test-win.c @@ -62,6 +62,7 @@ struct _PortalTestWin XdpPortal *portal; XdpSession *session; + XdpGlobalShortcutsSession *gs_session; GNetworkMonitor *monitor; GProxyResolver *resolver; @@ -91,6 +92,8 @@ struct _PortalTestWin GtkWidget *screencast_label; GtkWidget *screencast_toggle; + GtkWidget *globalshortcuts_activations; + GtkWidget *inputcapture_label; GtkWidget *inputcapture_toggle; @@ -715,6 +718,119 @@ capture_input_release (GtkButton *button, /* FIXME */ } + +static void +globalshortcuts_activated (XdpGlobalShortcutsSession *session, + const char *shortcut_id, + guint timestamp, + gpointer user_data) +{ + GtkLabel *label = GTK_LABEL (user_data); + gtk_label_set_text(label, shortcut_id); +} + +static void +globalshortcuts_bind_done (GObject *source, + GAsyncResult *result, + gpointer data) +{ + PortalTestWin *win = data; + g_autoptr(GError) error = NULL; + g_autoptr (GString) s = NULL; + XdpGlobalShortcutsSession *session = win->gs_session; + GArray *shortcuts; + + shortcuts = xdp_global_shortcuts_session_bind_shortcuts_finish(session, result, &error); + if (shortcuts == NULL) + { + g_warning ("Failed to bind GlobalShortcuts: %s", error->message); + gtk_label_set_label (GTK_LABEL (win->globalshortcuts_activations), "failed to bind"); + return; + } + + s = g_string_new (""); + for (guint i = 0; i < shortcuts->len; i++) + { + struct XdpGlobalShortcutAssigned shortcut = ((struct XdpGlobalShortcutAssigned*)shortcuts->data)[i]; + g_string_append_printf (s, "%s: %s ", shortcut.name, shortcut.trigger_description); + } + gtk_label_set_label (GTK_LABEL (win->globalshortcuts_activations), s->str); + + g_signal_connect (session, "activated", G_CALLBACK (globalshortcuts_activated), win->globalshortcuts_activations); +} + +static void +globalshortcuts_bind(PortalTestWin *win) +{ + struct XdpGlobalShortcut s[2] = {{.description = "Do Foo", .name = "foo", .preferred_trigger = "CTRL+F"}, + {.description = "Do Bar", .name = "bar", .preferred_trigger = NULL}}; + GArray shortcuts = {.data=(char*)s, .len=2}; + xdp_global_shortcuts_session_bind_shortcuts(win->gs_session, + &shortcuts, + NULL, + NULL, + globalshortcuts_bind_done, + win); +} + +static void +globalshortcuts_session_created (GObject *source, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (source); + PortalTestWin *win = data; + g_autoptr(GError) error = NULL; + XdpGlobalShortcutsSession *session; + + session = xdp_portal_create_global_shortcuts_session_finish (portal, result, &error); + if (session == NULL) + { + g_warning ("Failed to create GlobalShortcuts session: %s", error->message); + return; + } + win->gs_session = session; + + gtk_label_set_label (GTK_LABEL (win->globalshortcuts_activations), "created"); + globalshortcuts_bind(win); +} + +static void +globalshortcuts_session_start (PortalTestWin *win) +{ + g_clear_object (&win->gs_session); + + xdp_portal_create_global_shortcuts_session (win->portal, + NULL, + globalshortcuts_session_created, + win); +} + + +static void +globalshortcuts_session_stop (PortalTestWin *win) +{ + xdp_global_shortcuts_session_close (win->gs_session); + g_clear_object (&win->gs_session); + gtk_label_set_label (GTK_LABEL (win->globalshortcuts_activations), ""); +} + + + +static void +global_shortcuts_request (GtkButton *button, + PortalTestWin *win) +{ + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button))) + { + globalshortcuts_session_start (win); + } + else + { + globalshortcuts_session_stop (win); + } +} + static void session_started (GObject *source, GAsyncResult *result, @@ -737,6 +853,8 @@ session_started (GObject *source, return; } + + s = g_string_new (""); iter = g_variant_iter_new (xdp_session_get_streams (session)); @@ -1401,6 +1519,7 @@ portal_test_win_class_init (PortalTestWinClass *class) gtk_widget_class_bind_template_callback (widget_class, open_directory); gtk_widget_class_bind_template_callback (widget_class, open_local); gtk_widget_class_bind_template_callback (widget_class, take_screenshot); + gtk_widget_class_bind_template_callback (widget_class, global_shortcuts_request); gtk_widget_class_bind_template_callback (widget_class, capture_input); gtk_widget_class_bind_template_callback (widget_class, capture_input_release); gtk_widget_class_bind_template_callback (widget_class, screencast_toggled); @@ -1426,6 +1545,7 @@ portal_test_win_class_init (PortalTestWinClass *class) gtk_widget_class_bind_template_child (widget_class, PortalTestWin, inhibit_logout); gtk_widget_class_bind_template_child (widget_class, PortalTestWin, inhibit_suspend); gtk_widget_class_bind_template_child (widget_class, PortalTestWin, inhibit_switch); + gtk_widget_class_bind_template_child (widget_class, PortalTestWin, globalshortcuts_activations); gtk_widget_class_bind_template_child (widget_class, PortalTestWin, inputcapture_label); gtk_widget_class_bind_template_child (widget_class, PortalTestWin, inputcapture_toggle); gtk_widget_class_bind_template_child (widget_class, PortalTestWin, username); diff --git a/portal-test/gtk3/portal-test-win.ui b/portal-test/gtk3/portal-test-win.ui index 656bd8ac..352f1cd0 100644 --- a/portal-test/gtk3/portal-test-win.ui +++ b/portal-test/gtk3/portal-test-win.ui @@ -838,7 +838,40 @@ - + + + 1 + end + Global Shortcuts + + + 0 + 22 + + + + + 1 + 0 + Request + + + + 1 + 22 + + + + + 1 + end + activations + + + 2 + 22 + +