diff --git a/libportal/inputcapture-pointerbarrier.c b/libportal/inputcapture-pointerbarrier.c new file mode 100644 index 00000000..d904a6c4 --- /dev/null +++ b/libportal/inputcapture-pointerbarrier.c @@ -0,0 +1,248 @@ +/* + * 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 "portal-private.h" +#include "session-private.h" +#include "inputcapture-pointerbarrier.h" +#include "inputcapture-private.h" + +/** + * XdpInputCapturePointerBarrier + * + * A representation of a pointer barrier on an [class@InputCaptureZone]. + * Barriers can be assigned with + * [method@InputCaptureSession.set_pointer_barriers], once the Portal + * interaction is complete the barrier's "is-active" state indicates whether + * the barrier is active. Barriers can only be used once, subsequent calls to + * [method@InputCaptureSession.set_pointer_barriers] will invalidate all + * current barriers. + */ + +enum +{ + PROP_0, + + PROP_X1, + PROP_X2, + PROP_Y1, + PROP_Y2, + PROP_ID, + PROP_IS_ACTIVE, + + N_PROPERTIES +}; + +enum +{ + LAST_SIGNAL +}; + +enum barrier_state +{ + BARRIER_STATE_NEW, + BARRIER_STATE_ACTIVE, + BARRIER_STATE_FAILED, +}; + +static GParamSpec *properties[N_PROPERTIES] = { NULL, }; + +struct _XdpInputCapturePointerBarrier { + GObject parent_instance; + + unsigned int id; + int x1, y1; + int x2, y2; + + enum barrier_state state; +}; + +G_DEFINE_TYPE (XdpInputCapturePointerBarrier, xdp_input_capture_pointer_barrier, G_TYPE_OBJECT) + +static void +xdp_input_capture_pointer_barrier_get_property (GObject *object, + unsigned int property_id, + GValue *value, + GParamSpec *pspec) +{ + XdpInputCapturePointerBarrier *barrier = XDP_INPUT_CAPTURE_POINTER_BARRIER (object); + + switch (property_id) + { + case PROP_X1: + g_value_set_int (value, barrier->x1); + break; + case PROP_Y1: + g_value_set_int (value, barrier->y1); + break; + case PROP_X2: + g_value_set_int (value, barrier->x2); + break; + case PROP_Y2: + g_value_set_int (value, barrier->y2); + break; + case PROP_ID: + g_value_set_uint (value, barrier->id); + break; + case PROP_IS_ACTIVE: + g_value_set_boolean (value, barrier->state == BARRIER_STATE_ACTIVE); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +xdp_input_capture_pointer_barrier_set_property (GObject *object, + unsigned int property_id, + const GValue *value, + GParamSpec *pspec) +{ + XdpInputCapturePointerBarrier *pointerbarrier = XDP_INPUT_CAPTURE_POINTER_BARRIER (object); + + switch (property_id) + { + case PROP_X1: + pointerbarrier->x1 = g_value_get_int (value); + break; + case PROP_Y1: + pointerbarrier->y1 = g_value_get_int (value); + break; + case PROP_X2: + pointerbarrier->x2 = g_value_get_int (value); + break; + case PROP_Y2: + pointerbarrier->y2 = g_value_get_int (value); + break; + case PROP_ID: + pointerbarrier->id = g_value_get_uint (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +xdp_input_capture_pointer_barrier_class_init (XdpInputCapturePointerBarrierClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = xdp_input_capture_pointer_barrier_get_property; + object_class->set_property = xdp_input_capture_pointer_barrier_set_property; + + /** + * XdpInputCapturePointerBarrier:x1: + * + * The pointer barrier x offset in logical pixels + */ + properties[PROP_X1] = + g_param_spec_int ("x1", + "Pointer barrier x offset", + "The pointer barrier x offset in logical pixels", + INT_MIN, INT_MAX, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + + /** + * XdpInputCapturePointerBarrier:y1: + * + * The pointer barrier y offset in logical pixels + */ + properties[PROP_Y1] = + g_param_spec_int ("y1", + "Pointer barrier y offset", + "The pointer barrier y offset in logical pixels", + INT_MIN, INT_MAX, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + /** + * XdpInputCapturePointerBarrier:x2: + * + * The pointer barrier x offset in logical pixels + */ + properties[PROP_X2] = + g_param_spec_int ("x2", + "Pointer barrier x offset", + "The pointer barrier x offset in logical pixels", + INT_MIN, INT_MAX, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + /** + * XdpInputCapturePointerBarrier:y2: + * + * The pointer barrier y offset in logical pixels + */ + properties[PROP_Y2] = + g_param_spec_int ("y2", + "Pointer barrier y offset", + "The pointer barrier y offset in logical pixels", + INT_MIN, INT_MAX, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + /** + * XdpInputCapturePointerBarrier:id: + * + * The caller-assigned unique id of this barrier + */ + properties[PROP_ID] = + g_param_spec_uint ("id", + "Pointer barrier unique id", + "The id assigned to this barrier by the caller", + 0, UINT_MAX, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + /** + * XdpInputCapturePointerBarrier:is-active: + * + * A boolean indicating whether this barrier is active. A barrier cannot + * become active once it failed to apply, barriers that are not active can + * be thus cleaned up by the caller. + */ + properties[PROP_IS_ACTIVE] = + g_param_spec_boolean ("is-active", + "true if active, false otherwise", + "true if active, false otherwise", + FALSE, + G_PARAM_READABLE); + + g_object_class_install_properties (object_class, N_PROPERTIES, properties); +} + +static void +xdp_input_capture_pointer_barrier_init (XdpInputCapturePointerBarrier *barrier) +{ + barrier->state = BARRIER_STATE_NEW; +} + +unsigned int +_xdp_input_capture_pointer_barrier_get_id (XdpInputCapturePointerBarrier *barrier) +{ + return barrier->id; +} + +void +_xdp_input_capture_pointer_barrier_set_is_active (XdpInputCapturePointerBarrier *barrier, gboolean active) +{ + g_return_if_fail (barrier->state == BARRIER_STATE_NEW); + + if (active) + barrier->state = BARRIER_STATE_ACTIVE; + else + barrier->state = BARRIER_STATE_FAILED; + + g_object_notify_by_pspec (G_OBJECT (barrier), properties[PROP_IS_ACTIVE]); +} diff --git a/libportal/inputcapture-pointerbarrier.h b/libportal/inputcapture-pointerbarrier.h new file mode 100644 index 00000000..52db9bbf --- /dev/null +++ b/libportal/inputcapture-pointerbarrier.h @@ -0,0 +1,31 @@ +/* + * 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 + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER (xdp_input_capture_pointer_barrier_get_type ()) + +XDP_PUBLIC +G_DECLARE_FINAL_TYPE (XdpInputCapturePointerBarrier, xdp_input_capture_pointer_barrier, XDP, INPUT_CAPTURE_POINTER_BARRIER, GObject) + +G_END_DECLS diff --git a/libportal/inputcapture-private.h b/libportal/inputcapture-private.h new file mode 100644 index 00000000..e554df2c --- /dev/null +++ b/libportal/inputcapture-private.h @@ -0,0 +1,30 @@ +/* + * 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 "inputcapture-pointerbarrier.h" +#include "inputcapture-zone.h" + +guint +_xdp_input_capture_pointer_barrier_get_id (XdpInputCapturePointerBarrier *barrier); + +void +_xdp_input_capture_pointer_barrier_set_is_active (XdpInputCapturePointerBarrier *barrier, gboolean active); + +void +_xdp_input_capture_zone_invalidate_and_free (XdpInputCaptureZone *zone); diff --git a/libportal/inputcapture-zone.c b/libportal/inputcapture-zone.c new file mode 100644 index 00000000..7b27ede9 --- /dev/null +++ b/libportal/inputcapture-zone.c @@ -0,0 +1,238 @@ +/* + * 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 "inputcapture-zone.h" + +/** + * XdpInputCaptureZone + * + * A representation of a zone that supports input capture. + * + * The [class@XdpInputCaptureZone] object is used to represent a zone on the + * user-visible desktop that may be used to set up + * [class@XdpInputCapturePointerBarrier] objects. In most cases, the set of + * [class@XdpInputCaptureZone] objects represent the available monitors but the + * exact implementation is up to the implementation. + */ + +enum +{ + PROP_0, + + PROP_WIDTH, + PROP_HEIGHT, + PROP_X, + PROP_Y, + PROP_ZONE_SET, + PROP_IS_VALID, + + N_PROPERTIES +}; + +static GParamSpec *zone_properties[N_PROPERTIES] = { NULL, }; + +struct _XdpInputCaptureZone { + GObject parent_instance; + + unsigned int width; + unsigned int height; + int x; + int y; + + unsigned int zone_set; + + gboolean is_valid; +}; + +G_DEFINE_TYPE (XdpInputCaptureZone, xdp_input_capture_zone, G_TYPE_OBJECT) + +static void +xdp_input_capture_zone_get_property (GObject *object, + unsigned int property_id, + GValue *value, + GParamSpec *pspec) +{ + + XdpInputCaptureZone *zone = XDP_INPUT_CAPTURE_ZONE (object); + + switch (property_id) + { + case PROP_WIDTH: + g_value_set_uint (value, zone->width); + break; + case PROP_HEIGHT: + g_value_set_uint (value, zone->height); + break; + case PROP_X: + g_value_set_int (value, zone->x); + break; + case PROP_Y: + g_value_set_int (value, zone->y); + break; + case PROP_ZONE_SET: + g_value_set_uint (value, zone->zone_set); + break; + case PROP_IS_VALID: + g_value_set_boolean (value, zone->is_valid); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +xdp_input_capture_zone_set_property (GObject *object, + unsigned int property_id, + const GValue *value, + GParamSpec *pspec) +{ + XdpInputCaptureZone *zone = XDP_INPUT_CAPTURE_ZONE (object); + + switch (property_id) + { + case PROP_WIDTH: + zone->width = g_value_get_uint (value); + break; + case PROP_HEIGHT: + zone->height = g_value_get_uint (value); + break; + case PROP_X: + zone->x = g_value_get_int (value); + break; + case PROP_Y: + zone->y = g_value_get_int (value); + break; + case PROP_ZONE_SET: + zone->zone_set = g_value_get_uint (value); + break; + case PROP_IS_VALID: + zone->is_valid = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +xdp_input_capture_zone_class_init (XdpInputCaptureZoneClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = xdp_input_capture_zone_get_property; + object_class->set_property = xdp_input_capture_zone_set_property; + + /** + * XdpInputCaptureZone:width: + * + * The width of this zone in logical pixels + */ + zone_properties[PROP_WIDTH] = + g_param_spec_uint ("width", + "zone width", + "The zone width in logical pixels", + 0, UINT_MAX, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + + /** + * XdpInputCaptureZone:height: + * + * The height of this zone in logical pixels + */ + zone_properties[PROP_HEIGHT] = + g_param_spec_uint ("height", + "zone height", + "The zone height in logical pixels", + 0, UINT_MAX, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + + /** + * XdpInputCaptureZone:x: + * + * The x offset of this zone in logical pixels + */ + zone_properties[PROP_X] = + g_param_spec_int ("x", + "zone x offset", + "The zone x offset in logical pixels", + INT_MIN, INT_MAX, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + /** + * XdpInputCaptureZone:y: + * + * The x offset of this zone in logical pixels + */ + zone_properties[PROP_Y] = + g_param_spec_int ("y", + "zone y offset", + "The zone y offset in logical pixels", + INT_MIN, INT_MAX, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + + /** + * XdpInputCaptureZone:zone_set: + * + * The unique zone_set number assigned to this set of zones. A set of zones as + * returned by [method@InputCaptureSession.get_zones] have the same zone_set + * number and only one set of zones may be valid at any time (the most + * recently returned set). + */ + zone_properties[PROP_ZONE_SET] = + g_param_spec_uint ("zone_set", + "zone set number", + "The zone_set number when this zone was retrieved", + 0, UINT_MAX, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE); + + /** + * XdpInputCaptureZone:is-valid: + * + * A boolean indicating whether this zone is currently valid. Zones are + * invalidated by the Portal's ZonesChanged signal, see + * [signal@InputCaptureSession::zones-changed]. + * + * Once invalidated, a Zone can be discarded by the caller, it cannot become + * valid again. + */ + zone_properties[PROP_IS_VALID] = + g_param_spec_boolean ("is-valid", + "validity check", + "True if this zone is currently valid", + TRUE, + G_PARAM_READWRITE); + + g_object_class_install_properties (object_class, + N_PROPERTIES, + zone_properties); +} + +static void +xdp_input_capture_zone_init (XdpInputCaptureZone *zone) +{ +} + +void +_xdp_input_capture_zone_invalidate_and_free (XdpInputCaptureZone *zone) +{ + g_object_set (zone, "is-valid", FALSE, NULL); + g_object_unref (zone); +} diff --git a/libportal/inputcapture-zone.h b/libportal/inputcapture-zone.h new file mode 100644 index 00000000..88bfb513 --- /dev/null +++ b/libportal/inputcapture-zone.h @@ -0,0 +1,31 @@ +/* + * 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 + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define XDP_TYPE_INPUT_CAPTURE_ZONE (xdp_input_capture_zone_get_type ()) + +XDP_PUBLIC +G_DECLARE_FINAL_TYPE (XdpInputCaptureZone, xdp_input_capture_zone, XDP, INPUT_CAPTURE_ZONE, GObject) + +G_END_DECLS diff --git a/libportal/inputcapture.c b/libportal/inputcapture.c new file mode 100644 index 00000000..d9b097aa --- /dev/null +++ b/libportal/inputcapture.c @@ -0,0 +1,1199 @@ +/* + * 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 "inputcapture.h" +#include "inputcapture-private.h" +#include "portal-private.h" +#include "session-private.h" + +/** + * XdpInputCaptureSession + * + * A representation of a long-lived input capture portal interaction. + * + * The [class@InputCaptureSession] object is used to represent portal + * interactions with the input capture desktop portal that extend over + * multiple portal calls. Usually a caller creates an input capture session, + * requests the available zones and sets up pointer barriers on those zones + * before enabling the session. + * + * To find available zones, call [method@InputCaptureSession.get_zones]. + * These [class@InputCaptureZone] object represent the accessible desktop area + * for input capturing. [class@InputCapturePointerBarrier] objects can be set + * up on these zones to trigger input capture. + * + * The [class@InputCaptureSession] wraps a [class@Session] object. + */ + +enum { + SIGNAL_CLOSED, + SIGNAL_ACTIVATED, + SIGNAL_DEACTIVATED, + SIGNAL_ZONES_CHANGED, + SIGNAL_DISABLED, + SIGNAL_LAST_SIGNAL +}; + +static guint signals[SIGNAL_LAST_SIGNAL]; + +struct _XdpInputCaptureSession +{ + GObject parent_instance; + XdpSession *parent_session; /* strong ref */ + + GList *zones; + + guint signal_ids[SIGNAL_LAST_SIGNAL]; + guint zone_serial; + guint zone_set; +}; + +G_DEFINE_TYPE (XdpInputCaptureSession, xdp_input_capture_session, G_TYPE_OBJECT) + +static gboolean +_xdp_input_capture_session_is_valid (XdpInputCaptureSession *session) +{ + return XDP_IS_INPUT_CAPTURE_SESSION (session) && session->parent_session != NULL; +} + +static void +parent_session_destroy (gpointer data, GObject *old_session) +{ + XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_SESSION (data); + + g_critical ("XdpSession destroyed before XdpInputCaptureSesssion, you lost count of your session refs"); + + session->parent_session = NULL; +} + +static void +xdp_input_capture_session_finalize (GObject *object) +{ + XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_SESSION (object); + XdpSession *parent_session = session->parent_session; + + if (parent_session == NULL) + { + g_critical ("XdpSession destroyed before XdpInputCaptureSesssion, 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); + session->parent_session->input_capture_session = NULL; + g_clear_pointer (&session->parent_session, g_object_unref); + } + + g_list_free_full (g_steal_pointer (&session->zones), g_object_unref); + + G_OBJECT_CLASS (xdp_input_capture_session_parent_class)->finalize (object); +} + +static void +xdp_input_capture_session_class_init (XdpInputCaptureSessionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = xdp_input_capture_session_finalize; + + /** + * XdpInputCaptureSession::zones-changed: + * @session: the [class@InputCaptureSession] + * @options: a GVariant with the signal options + * + * Emitted when an InputCapture session's zones have changed. When this + * signal is emitted, all current zones will have their + * [property@InputCaptureZone:is-valid] property set to %FALSE and all + * internal references to those zones have been released. This signal is + * sent after libportal has fetched the updated zones, a caller should call + * xdp_input_capture_session_get_zones() to retrieve the new zones. + */ + signals[SIGNAL_ZONES_CHANGED] = + g_signal_new ("zones-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); + /** + * XdpInputCaptureSession::activated: + * @session: the [class@InputCaptureSession] + * @activation_id: the unique activation_id to identify this input capture + * @options: a GVariant with the signal options + * + * Emitted when an InputCapture session activates and sends events. When this + * signal is emitted, events will appear on the transport layer. + */ + 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_UINT, + G_TYPE_VARIANT); + /** + * XdpInputCaptureSession::deactivated: + * @session: the [class@InputCaptureSession] + * @activation_id: the unique activation_id to identify this input capture + * @options: a GVariant with the signal options + * + * Emitted when an InputCapture session deactivates and no longer sends + * events. + */ + 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_UINT, + G_TYPE_VARIANT); + + /** + * XdpInputCaptureSession::disabled: + * @session: the [class@InputCaptureSession] + * @options: a GVariant with the signal options + * + * Emitted when an InputCapture session is disabled. This signal + * is emitted when capturing was disabled by the server. + */ + signals[SIGNAL_DISABLED] = + g_signal_new ("disabled", + 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); +} + +static void +xdp_input_capture_session_init (XdpInputCaptureSession *session) +{ + session->parent_session = NULL; + session->zones = NULL; + session->zone_set = 0; + 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 */ + + /* CreateSession only */ + XdpParent *parent; + char *parent_handle; + XdpInputCapability capabilities; + + /* GetZones only */ + XdpInputCaptureSession *session; + + /* SetPointerBarrier only */ + GList *barriers; + +} Call; + +static void create_session (Call *call); +static void get_zones (Call *call); + +static void +call_free (Call *call) +{ + /* CreateSesssion */ + if (call->parent) + { + call->parent->parent_unexport (call->parent); + xdp_parent_free (call->parent); + } + g_free (call->parent_handle); + + /* 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) + { + g_clear_signal_handler (&call->cancelled_id, g_task_get_cancellable (call->task)); + g_task_return_error (call->task, error); + call_free (call); + } +} + +static gboolean +handle_matches_session (XdpInputCaptureSession *session, const char *id) +{ + const char *sid = session->parent_session->id; + + return g_str_equal (sid, id); +} + +static void +set_zones (XdpInputCaptureSession *session, GVariant *zones, guint zone_set) +{ + GList *list = NULL; + gsize nzones = g_variant_n_children (zones); + + for (gsize i = 0; i < nzones; i++) + { + guint width, height; + gint x, y; + XdpInputCaptureZone *z; + + g_variant_get_child (zones, i, "(uuii)", &width, &height, &x, &y); + + z = g_object_new (XDP_TYPE_INPUT_CAPTURE_ZONE, + "width", width, + "height", height, + "x", x, + "y", y, + "zone-set", zone_set, + "is-valid", TRUE, + NULL); + list = g_list_append (list, z); + } + + g_list_free_full (g_steal_pointer (&session->zones), (GDestroyNotify)_xdp_input_capture_zone_invalidate_and_free); + session->zones = list; + session->zone_set = zone_set; +} + + +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 +zones_changed_emit_signal (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_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_ZONES_CHANGED], 0, g_variant_new ("a{sv}", &options)); +} + +static void +zones_changed (GDBusConnection *bus, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer data) +{ + XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_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, zones_changed_emit_signal, session); + call->session = g_object_ref (session); + + get_zones (call); +} + +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) +{ + XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_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) +{ + XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_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 void +disabled (GDBusConnection *bus, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer data) +{ + XdpInputCaptureSession *session = XDP_INPUT_CAPTURE_SESSION (data); + g_autoptr(GVariant) options = NULL; + const char *handle = NULL; + + g_variant_get(parameters, "(o@a{sv})", &handle, &options); + + if (!handle_matches_session (session, handle)) + return; + + g_signal_emit (session, signals[SIGNAL_DISABLED], 0, options); +} + +static XdpInputCaptureSession * +_xdp_input_capture_session_new (XdpPortal *portal, const char *session_path) +{ + g_autoptr(XdpSession) parent_session = _xdp_session_new (portal, session_path, XDP_SESSION_INPUT_CAPTURE); + g_autoptr(XdpInputCaptureSession) session = g_object_new (XDP_TYPE_INPUT_CAPTURE_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); +} + +static void +get_zones_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) + { + GVariant *zones = NULL; + guint32 zone_set; + XdpInputCaptureSession *session = call->session; + + g_dbus_connection_signal_unsubscribe (call->portal->bus, call->signal_id); + call->signal_id = 0; + + if (session == NULL) + { + session = _xdp_input_capture_session_new (call->portal, call->session_path); + session->signal_ids[SIGNAL_ZONES_CHANGED] = + g_dbus_connection_signal_subscribe (bus, + PORTAL_BUS_NAME, + "org.freedesktop.portal.InputCapture", + "ZonesChanged", + PORTAL_OBJECT_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, + zones_changed, + session, + NULL); + + session->signal_ids[SIGNAL_ACTIVATED] = + g_dbus_connection_signal_subscribe (bus, + PORTAL_BUS_NAME, + "org.freedesktop.portal.InputCapture", + "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.InputCapture", + "Deactivated", + PORTAL_OBJECT_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, + deactivated, + session, + NULL); + + session->signal_ids[SIGNAL_DISABLED] = + g_dbus_connection_signal_subscribe (bus, + PORTAL_BUS_NAME, + "org.freedesktop.portal.InputCapture", + "Disabled", + PORTAL_OBJECT_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, + disabled, + session, + NULL); + } + + if (g_variant_lookup (ret, "zone_set", "u", &zone_set) && + g_variant_lookup (ret, "zones", "@a(uuii)", &zones)) + { + set_zones (session, zones, zone_set); + g_task_return_pointer (call->task, session, g_object_unref); + } + else + { + g_warning("Faulty portal implementation, missing GetZone's zone_set or zones"); + response = 2; + } + } + + if (response == 1) + g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, "InputCapture GetZones() canceled"); + else if (response == 2) + g_task_return_new_error (call->task, G_IO_ERROR, G_IO_ERROR_FAILED, "InputCapture GetZones() failed"); + + if (response != 0) + call_free (call); +} + +static void +get_zones (Call *call) +{ + GVariantBuilder options; + const char *session_id; + + /* May be called after CreateSession before we have an XdpInputCaptureSession, or by the + * ZoneChanged signal when we do have a session */ + session_id = call->session ? call->session->parent_session->id : call->session_path; + + prep_call (call, get_zones_done, &options, NULL); + g_dbus_connection_call (call->portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.portal.InputCapture", + "GetZones", + g_variant_new ("(oa{sv})", session_id, &options), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + g_task_get_cancellable (call->task), + call_returned, + 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", "o", &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 + get_zones (call); + } + 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 +parent_exported (XdpParent *parent, + const char *handle, + gpointer data) +{ + Call *call = data; + call->parent_handle = g_strdup (handle); + create_session (call); +} + +static void +create_session (Call *call) +{ + GVariantBuilder options; + g_autofree char *session_token = NULL; + GCancellable *cancellable; + + if (call->parent_handle == NULL) + { + call->parent->parent_export (call->parent, parent_exported, call); + return; + } + + 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_variant_builder_add (&options, "{sv}", "capabilities", g_variant_new_uint32 (call->capabilities)); + + g_dbus_connection_call (call->portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.portal.InputCapture", + "CreateSession", + g_variant_new ("(sa{sv})", call->parent_handle, &options), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + cancellable, + call_returned, + call); +} + +/** + * xdp_portal_create_input_capture_session: + * @portal: a [class@Portal] + * @parent: (nullable): parent window information + * @capabilities: which kinds of capabilities to request + * @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 input capture + * + * When the request is done, @callback will be called. You can then + * call [method@Portal.create_input_capture_session_finish] to get the results. + */ +void +xdp_portal_create_input_capture_session (XdpPortal *portal, + XdpParent *parent, + XdpInputCapability capabilities, + 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); + + if (parent) + call->parent = xdp_parent_copy (parent); + else + call->parent_handle = g_strdup (""); + + call->capabilities = capabilities; + + create_session (call); +} + +/** + * xdp_portal_create_input_capture_session_finish: + * @portal: a [class@Portal] + * @result: a [iface@Gio.AsyncResult] + * @error: return location for an error + * + * Finishes the InputCapture CreateSession request, and returns a + * [class@InputCaptureSession]. To get to the [class@Session] within use + * xdp_input_capture_session_get_session(). + * + * Returns: (transfer full): a [class@InputCaptureSession] + */ +XdpInputCaptureSession * +xdp_portal_create_input_capture_session_finish (XdpPortal *portal, + GAsyncResult *result, + GError **error) +{ + XdpInputCaptureSession *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_input_capture_session_get_session: + * @session: a [class@XdpInputCaptureSession] + * + * Return the [class@XdpSession] for this InputCapture session. + * + * Returns: (transfer none): a [class@Session] object + */ +XdpSession * +xdp_input_capture_session_get_session (XdpInputCaptureSession *session) +{ + return session->parent_session; +} + +/** + * xdp_input_capture_session_get_zones: + * @session: a [class@InputCaptureSession] + * + * Obtains the current set of [class@InputCaptureZone] objects. + * + * The returned object is valid until the zones are invalidated by the + * [signal@InputCaptureSession::zones-changed] signal. + * + * Unless the session is active, this function returns `NULL`. + * + * Returns: (element-type XdpInputCaptureZone) (transfer none): the available + * zones. The caller must keep a reference to the list or the elements if used + * outside the immediate scope. + */ +GList * +xdp_input_capture_session_get_zones (XdpInputCaptureSession *session) +{ + g_return_val_if_fail (_xdp_input_capture_session_is_valid (session), NULL); + + return session->zones; +} + +/** + * xdp_input_capture_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_input_capture_session_connect_to_eis (XdpInputCaptureSession *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_input_capture_session_is_valid (session)) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, "Session is not an InputCapture 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 +set_pointer_barriers_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; + GVariant *failed = NULL; + GList *failed_list = NULL; + + g_variant_get (parameters, "(u@a{sv})", &response, &ret); + + if (g_variant_lookup (ret, "failed_barriers", "@au", &failed)) + { + const guint *failed_barriers = NULL; + gsize n_elements; + GList *it = call->barriers; + + failed_barriers = g_variant_get_fixed_array (failed, &n_elements, sizeof (guint32)); + + while (it) + { + XdpInputCapturePointerBarrier *b = it->data; + gboolean is_failed = FALSE; + + for (gsize i = 0; !is_failed && i < n_elements; i++) + is_failed = _xdp_input_capture_pointer_barrier_get_id (b) == failed_barriers[i]; + + _xdp_input_capture_pointer_barrier_set_is_active (b, !is_failed); + + if (is_failed) + failed_list = g_list_append (failed_list, g_object_ref(b)); + + it = it->next; + } + } + + /* all failed barriers have an extra ref in failed_list, so we can unref all barriers + in our original list */ + free_barrier_list (call->barriers); + call->barriers = NULL; + g_task_return_pointer (call->task, failed_list, (GDestroyNotify)free_barrier_list); +} + +static void +convert_barrier (gpointer data, gpointer user_data) +{ + XdpInputCapturePointerBarrier *barrier = data; + GVariantBuilder *builder = user_data; + GVariantBuilder dict; + int id, x1, x2, y1, y2; + + g_object_get (barrier, "id", &id, "x1", &x1, "x2", &x2, "y1", &y1, "y2", &y2, NULL); + + g_variant_builder_init (&dict, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&dict, "{sv}", "barrier_id", g_variant_new_int32 (id)); + g_variant_builder_add (&dict, "{sv}", "position", + g_variant_new("(iiii)", x1, y1, x2, y2)); + g_variant_builder_add (builder, "a{sv}", &dict); +} + +static void +set_pointer_barriers (Call *call) +{ + GVariantBuilder options; + GVariantBuilder barriers; + g_autoptr(GVariantType) vtype; + + prep_call (call, set_pointer_barriers_done, &options, NULL); + + vtype = g_variant_type_new ("aa{sv}"); + + g_variant_builder_init (&barriers, vtype); + g_list_foreach (call->barriers, convert_barrier, &barriers); + + g_dbus_connection_call (call->portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.portal.InputCapture", + "SetPointerBarriers", + g_variant_new ("(oa{sv}aa{sv}u)", + call->session->parent_session->id, + &options, + &barriers, + call->session->zone_set), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + g_task_get_cancellable (call->task), + call_returned, + call); +} + +static void +gobject_ref_wrapper (gpointer data, gpointer user_data) +{ + g_object_ref (G_OBJECT (data)); +} + +/** + * xdp_input_capture_session_set_pointer_barriers: + * @session: a [class@InputCaptureSession] + * @barriers: (element-type XdpInputCapturePointerBarrier) (transfer container): the pointer barriers to apply + * + * Sets the pointer barriers for this session. When the request is done, + * @callback will be called. You can then call + * [method@InputCaptureSession.set_pointer_barriers_finish] to + * get the results. The result of this request is the list of pointer barriers + * that failed to apply - barriers not present in the returned list are active. + * + * Once the pointer barrier is + * applied (i.e. the reply to the DBus Request has been received), the + * the [property@InputCapturePointerBarrier:is-active] property is changed on + * that barrier. Failed barriers have the property set to a %FALSE value. + */ +void +xdp_input_capture_session_set_pointer_barriers (XdpInputCaptureSession *session, + GList *barriers, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data) +{ + Call *call; + XdpPortal *portal; + + g_return_if_fail (_xdp_input_capture_session_is_valid (session)); + g_return_if_fail (barriers != NULL); + + portal = session->parent_session->portal; + + /* The list is ours, but we ref each object so we can create the list for the + * returned barriers during _finish*/ + g_list_foreach (barriers, gobject_ref_wrapper, NULL); + + 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); + call->barriers = barriers; + + set_pointer_barriers (call); +} + +/** + * xdp_input_capture_session_set_pointer_barriers_finish: + * @session: a [class@InputCaptureSession] + * @result: a [iface@Gio.AsyncResult] + * @error: return location for an error + * + * Finishes the set-pointer-barriers request, and returns a GList with the pointer + * barriers that failed to apply and should be cleaned up by the caller. + * + * Returns: (element-type XdpInputCapturePointerBarrier) (transfer full): a list of failed pointer barriers + */ + +GList * +xdp_input_capture_session_set_pointer_barriers_finish (XdpInputCaptureSession *session, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (_xdp_input_capture_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); +} + +/** + * xdp_input_capture_session_enable: + * @session: a [class@InputCaptureSession] + * + * Enables this input capture session. In the future, this client may receive + * input events. + */ +void +xdp_input_capture_session_enable (XdpInputCaptureSession *session) +{ + XdpPortal *portal; + GVariantBuilder options; + + g_return_if_fail (_xdp_input_capture_session_is_valid (session)); + + portal = session->parent_session->portal; + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + + g_dbus_connection_call (portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.portal.InputCapture", + "Enable", + g_variant_new ("(oa{sv})", + session->parent_session->id, + &options), + NULL, + G_DBUS_CALL_FLAGS_NONE, + 1, + NULL, + NULL, + NULL); +} + +/** + * xdp_input_capture_session_disable: + * @session: a [class@InputCaptureSession] + * + * Disables this input capture session. + */ +void +xdp_input_capture_session_disable (XdpInputCaptureSession *session) +{ + XdpPortal *portal; + GVariantBuilder options; + + g_return_if_fail (_xdp_input_capture_session_is_valid (session)); + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + + portal = session->parent_session->portal; + g_dbus_connection_call (portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.portal.InputCapture", + "Disable", + g_variant_new ("(oa{sv})", + session->parent_session->id, + &options), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + NULL); +} + +static void +release_session (XdpInputCaptureSession *session, + guint activation_id, + gboolean with_position, + gdouble x, + gdouble y) +{ + XdpPortal *portal; + GVariantBuilder options; + + g_return_if_fail (_xdp_input_capture_session_is_valid (session)); + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&options, "{sv}", "activation_id", g_variant_new_uint32 (activation_id)); + + if (with_position) + { + g_variant_builder_add (&options, + "{sv}", + "cursor_position", + g_variant_new ("(dd)", x, y)); + } + + portal = session->parent_session->portal; + g_dbus_connection_call (portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.portal.InputCapture", + "Release", + g_variant_new ("(oa{sv})", + session->parent_session->id, + &options), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + NULL); +} + +/** + * xdp_input_capture_session_release: + * @session: a [class@InputCaptureSession] + * + * Releases this input capture session without a suggested cursor position. + */ +void +xdp_input_capture_session_release (XdpInputCaptureSession *session, + guint activation_id) +{ + g_return_if_fail (_xdp_input_capture_session_is_valid (session)); + + release_session (session, activation_id, FALSE, 0, 0); +} + +/** + * xdp_input_capture_session_release_at: + * @session: a [class@InputCaptureSession] + * @cursor_x_position: the suggested cursor x position once capture has been released + * @cursor_y_position: the suggested cursor y position once capture has been released + * + * Releases this input capture session with a suggested cursor position. + * Note that the implementation is not required to honour this position. + */ +void +xdp_input_capture_session_release_at (XdpInputCaptureSession *session, + guint activation_id, + gdouble cursor_x_position, + gdouble cursor_y_position) +{ + g_return_if_fail (_xdp_input_capture_session_is_valid (session)); + + release_session (session, activation_id, TRUE, cursor_x_position, cursor_y_position); +} diff --git a/libportal/inputcapture.h b/libportal/inputcapture.h new file mode 100644 index 00000000..fff94687 --- /dev/null +++ b/libportal/inputcapture.h @@ -0,0 +1,103 @@ +/* + * 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 +#include +#include + +G_BEGIN_DECLS + +#define XDP_TYPE_INPUT_CAPTURE_SESSION (xdp_input_capture_session_get_type ()) + +XDP_PUBLIC +G_DECLARE_FINAL_TYPE (XdpInputCaptureSession, xdp_input_capture_session, XDP, INPUT_CAPTURE_SESSION, GObject) + +/** + * XdpInputCapability: + * @XDP_INPUT_CAPABILITY_NONE: no device + * @XDP_INPUT_CAPABILITY_KEYBOARD: capture the keyboard + * @XDP_INPUT_CAPABILITY_POINTER: capture pointer events + * @XDP_INPUT_CAPABILITY_TOUCHSCREEN: capture touchscreen events + * + * Flags to specify what input device capabilities should be captured + */ +typedef enum { + XDP_INPUT_CAPABILITY_NONE = 0, + XDP_INPUT_CAPABILITY_KEYBOARD = 1 << 0, + XDP_INPUT_CAPABILITY_POINTER = 1 << 1, + XDP_INPUT_CAPABILITY_TOUCHSCREEN = 1 << 2 +} XdpInputCapability; + + +XDP_PUBLIC +void xdp_portal_create_input_capture_session (XdpPortal *portal, + XdpParent *parent, + XdpInputCapability capabilities, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data); + +XDP_PUBLIC +XdpInputCaptureSession * xdp_portal_create_input_capture_session_finish (XdpPortal *portal, + GAsyncResult *result, + GError **error); + +XDP_PUBLIC +XdpSession *xdp_input_capture_session_get_session (XdpInputCaptureSession *session); + +XDP_PUBLIC +GList * xdp_input_capture_session_get_zones (XdpInputCaptureSession *session); + +XDP_PUBLIC +void xdp_input_capture_session_set_pointer_barriers (XdpInputCaptureSession *session, + GList *barriers, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data); + +XDP_PUBLIC +GList * xdp_input_capture_session_set_pointer_barriers_finish (XdpInputCaptureSession *session, + GAsyncResult *result, + GError **error); + +XDP_PUBLIC +void xdp_input_capture_session_enable (XdpInputCaptureSession *session); + +XDP_PUBLIC +void xdp_input_capture_session_disable (XdpInputCaptureSession *session); + +XDP_PUBLIC +void xdp_input_capture_session_release_at (XdpInputCaptureSession *session, + guint activation_id, + gdouble cursor_x_position, + gdouble cursor_y_position); + +XDP_PUBLIC +void xdp_input_capture_session_release (XdpInputCaptureSession *session, + guint activation_id); + +XDP_PUBLIC +int xdp_input_capture_session_connect_to_eis (XdpInputCaptureSession *session, + GError **error); + +G_END_DECLS diff --git a/libportal/meson.build b/libportal/meson.build index ae47bd7c..7128a990 100644 --- a/libportal/meson.build +++ b/libportal/meson.build @@ -12,6 +12,9 @@ headers = [ 'email.h', 'filechooser.h', 'inhibit.h', + 'inputcapture.h', + 'inputcapture-zone.h', + 'inputcapture-pointerbarrier.h', 'location.h', 'notification.h', 'openuri.h', @@ -44,6 +47,9 @@ src = [ 'email.c', 'filechooser.c', 'inhibit.c', + 'inputcapture.c', + 'inputcapture-zone.c', + 'inputcapture-pointerbarrier.c', 'location.c', 'notification.c', 'openuri.c', diff --git a/libportal/portal.h b/libportal/portal.h index a3fc790e..3618b81e 100644 --- a/libportal/portal.h +++ b/libportal/portal.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include diff --git a/libportal/session-private.h b/libportal/session-private.h index d4c2fce1..c4525204 100644 --- a/libportal/session-private.h +++ b/libportal/session-private.h @@ -20,6 +20,7 @@ #pragma once #include +#include struct _XdpSession { GObject parent_instance; @@ -41,6 +42,8 @@ struct _XdpSession { gboolean uses_eis; + /* InputCapture */ + XdpInputCaptureSession *input_capture_session; /* weak ref */ }; XdpSession * _xdp_session_new (XdpPortal *portal, diff --git a/libportal/session.c b/libportal/session.c index 7de9278d..a068851f 100644 --- a/libportal/session.c +++ b/libportal/session.c @@ -58,6 +58,9 @@ xdp_session_finalize (GObject *object) g_clear_pointer (&session->restore_token, g_free); g_clear_pointer (&session->id, g_free); g_clear_pointer (&session->streams, g_variant_unref); + if (session->input_capture_session != NULL) + g_critical ("XdpSession destroyed before XdpInputCaptureSesssion, you lost count of your session refs"); + session->input_capture_session = NULL; G_OBJECT_CLASS (xdp_session_parent_class)->finalize (object); } @@ -115,6 +118,7 @@ _xdp_session_new (XdpPortal *portal, session->id = g_strdup (id); session->type = type; session->state = XDP_SESSION_INITIAL; + session->input_capture_session = NULL; session->signal_id = g_dbus_connection_signal_subscribe (portal->bus, PORTAL_BUS_NAME, diff --git a/libportal/session.h b/libportal/session.h index 3d85bacf..e9f02145 100644 --- a/libportal/session.h +++ b/libportal/session.h @@ -32,12 +32,14 @@ G_DECLARE_FINAL_TYPE (XdpSession, xdp_session, XDP, SESSION, GObject) * XdpSessionType: * @XDP_SESSION_SCREENCAST: a screencast session. * @XDP_SESSION_REMOTE_DESKTOP: a remote desktop session. + * @XDP_SESSION_INPUT_CAPTURE: an input capture session. * * The type of a session. */ typedef enum { XDP_SESSION_SCREENCAST, XDP_SESSION_REMOTE_DESKTOP, + XDP_SESSION_INPUT_CAPTURE, } XdpSessionType; XDP_PUBLIC diff --git a/portal-test/gtk3/portal-test-win.c b/portal-test/gtk3/portal-test-win.c index 74b0fefa..eeff9df7 100644 --- a/portal-test/gtk3/portal-test-win.c +++ b/portal-test/gtk3/portal-test-win.c @@ -63,6 +63,9 @@ struct _PortalTestWin GtkWidget *screencast_label; GtkWidget *screencast_toggle; + GtkWidget *inputcapture_label; + GtkWidget *inputcapture_toggle; + GFileMonitor *update_monitor; GtkWidget *update_dialog; GtkWidget *update_dialog2; @@ -156,7 +159,7 @@ update_available (XdpPortal *portal, PortalTestWin *win) { g_message ("Update available"); - + gtk_label_set_label (GTK_LABEL (win->update_label), "Update available"); gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (win->update_progressbar), 0.0); @@ -188,7 +191,7 @@ update_progress (XdpPortal *portal, } if (status != XDP_UPDATE_STATUS_RUNNING) - g_signal_handlers_disconnect_by_func (win->portal, update_progress, win); + g_signal_handlers_disconnect_by_func (win->portal, update_progress, win); } static void @@ -298,7 +301,7 @@ opened_uri (GObject *object, gboolean res; open_dir = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (win->open_local_dir)); - + if (open_dir) res = xdp_portal_open_directory_finish (portal, result, &error); else @@ -561,6 +564,87 @@ take_screenshot (GtkButton *button, xdp_parent_free (parent); } +static void +inputcapture_session_created (GObject *source, + GAsyncResult *result, + gpointer data) +{ + XdpPortal *portal = XDP_PORTAL (source); + PortalTestWin *win = data; + g_autoptr(GError) error = NULL; + GList *zones; + g_autoptr (GString) s = NULL; + XdpInputCaptureSession *ic; + + ic = xdp_portal_create_input_capture_session_finish (portal, result, &error); + if (ic == NULL) + { + g_warning ("Failed to create inputcapture session: %s", error->message); + return; + } + win->session = XDP_SESSION (ic); + + zones = xdp_input_capture_session_get_zones (XDP_INPUT_CAPTURE_SESSION (win->session)); + s = g_string_new (""); + for (GList *elem = g_list_first (zones); elem; elem = g_list_next (elem)) + { + XdpInputCaptureZone *zone = elem->data; + guint w, h; + gint x, y; + + g_object_get (zone, + "width", &w, + "height", &h, + "x", &x, + "y", &y, + NULL); + + g_string_append_printf (s, "%ux%u@%d,%d ", w, h, x, y); + } + gtk_label_set_label (GTK_LABEL (win->inputcapture_label), s->str); +} + +static void +start_input_capture (PortalTestWin *win) +{ + g_clear_object (&win->session); + + xdp_portal_create_input_capture_session (win->portal, + NULL, + XDP_INPUT_CAPABILITY_POINTER | XDP_INPUT_CAPABILITY_KEYBOARD, + NULL, + inputcapture_session_created, + win); +} + +static void +stop_input_capture (PortalTestWin *win) +{ + if (win->session != NULL) + { + xdp_session_close (win->session); + g_clear_object (&win->session); + gtk_label_set_label (GTK_LABEL (win->inputcapture_label), ""); + } +} + +static void +capture_input (GtkButton *button, + PortalTestWin *win) +{ + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button))) + start_input_capture (win); + else + stop_input_capture (win); +} + +static void +capture_input_release (GtkButton *button, + PortalTestWin *win) +{ + /* FIXME */ +} + static void session_started (GObject *source, GAsyncResult *result, @@ -599,7 +683,7 @@ session_started (GObject *source, gtk_label_set_label (GTK_LABEL (win->screencast_label), s->str); } - + static void session_created (GObject *source, GAsyncResult *result, @@ -616,7 +700,7 @@ session_created (GObject *source, g_warning ("Failed to create screencast session: %s", error->message); return; } - + parent = xdp_parent_new_gtk (GTK_WINDOW (win)); xdp_session_start (win->session, parent, NULL, session_started, win); xdp_parent_free (parent); @@ -650,7 +734,7 @@ stop_screencast (PortalTestWin *win) } static void -screencast_toggled (GtkToggleButton *button, +screencast_toggled (GtkToggleButton *button, PortalTestWin *win) { if (gtk_toggle_button_get_active (button)) @@ -726,7 +810,7 @@ compose_email_called (GObject *source, PortalTestWin *win = data; g_autoptr(GError) error = NULL; - if (!xdp_portal_compose_email_finish (win->portal, result, &error)) + if (!xdp_portal_compose_email_finish (win->portal, result, &error)) { g_warning ("Email error: %s", error->message); return; @@ -1247,6 +1331,8 @@ 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, capture_input); + gtk_widget_class_bind_template_callback (widget_class, capture_input_release); gtk_widget_class_bind_template_callback (widget_class, screencast_toggled); gtk_widget_class_bind_template_callback (widget_class, notify_me); gtk_widget_class_bind_template_callback (widget_class, print_cb); @@ -1269,6 +1355,8 @@ 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, inputcapture_label); + gtk_widget_class_bind_template_child (widget_class, PortalTestWin, inputcapture_toggle); gtk_widget_class_bind_template_child (widget_class, PortalTestWin, username); gtk_widget_class_bind_template_child (widget_class, PortalTestWin, realname); gtk_widget_class_bind_template_child (widget_class, PortalTestWin, avatar); diff --git a/portal-test/gtk3/portal-test-win.ui b/portal-test/gtk3/portal-test-win.ui index 7112a190..e449c298 100644 --- a/portal-test/gtk3/portal-test-win.ui +++ b/portal-test/gtk3/portal-test-win.ui @@ -726,6 +726,68 @@ 19 + + + + 1 + end + Input Capture + + + 0 + 20 + + + + + 1 + end + + + 2 + 20 + + + + + 1 + 0 + horizontal + 6 + + + 1 + 1 + Input Capture + + + + + + 1 + Enable + + + + + 1 + 20 + + + + + 1 + 1 + Release + + + + 2 + 20 + + + + diff --git a/tests/pyportaltest/templates/inputcapture.py b/tests/pyportaltest/templates/inputcapture.py new file mode 100644 index 00000000..9b0b6dc0 --- /dev/null +++ b/tests/pyportaltest/templates/inputcapture.py @@ -0,0 +1,367 @@ +# SPDX-License-Identifier: LGPL-3.0-only +# +# This file is formatted with Python Black + +"""xdg desktop portals mock template""" + +from pyportaltest.templates import Request, Response, ASVType, Session +from typing import Callable, Dict, List, Tuple, Iterator +from itertools import count + +import dbus +import dbus.service +import logging +import sys + +from gi.repository import GLib + +BUS_NAME = "org.freedesktop.portal.Desktop" +MAIN_OBJ = "/org/freedesktop/portal/desktop" +SYSTEM_BUS = False +MAIN_IFACE = "org.freedesktop.portal.InputCapture" + +logger = logging.getLogger(f"templates.{__name__}") +logger.setLevel(logging.DEBUG) + +zone_set = None +eis_serial = None + + +def load(mock, parameters=None): + + logger.debug(f"Loading parameters: {parameters}") + + # Delay before Request.response, applies to all functions + mock.delay: int = parameters.get("delay", 0) + + # EIS serial number, < 0 means "don't send a serial" + eis_serial_start = parameters.get("eis-serial", 0) + if eis_serial_start >= 0: + global eis_serial + eis_serial = count(start=eis_serial_start) + + # Zone set number, < 0 means "don't send a zone_set" + zone_set_start = parameters.get("zone-set", 0) + if zone_set_start >= 0: + global zone_set + zone_set = count(start=zone_set_start) + mock.current_zone_set = next(zone_set) + else: + mock.current_zone_set = None + + # An all-zeroes zone means "don't send a zone" + mock.current_zones = parameters.get("zones", ((1920, 1080, 0, 0),)) + if mock.current_zones[0] == (0, 0, 0, 0): + mock.current_zones = None + + # second set of zones after the change signal + mock.changed_zones = parameters.get("changed-zones", ((0, 0, 0, 0),)) + if mock.changed_zones[0] == (0, 0, 0, 0): + mock.changed_zones = None + + # milliseconds until the zones change to the changed_zones + mock.change_zones_after = parameters.get("change-zones-after", 0) + + # List of barrier ids to fail + mock.failed_barriers = parameters.get("failed-barriers", []) + + # When to send the Activated signal (in ms after Enable), 0 means no + # signal + mock.activated_after = parameters.get("activated-after", 0) + + # Barrier ID that triggers Activated (-1 means don't add barrier id) + mock.activated_barrier = parameters.get("activated-barrier", None) + + # Position tuple for Activated signal, None means don't add position + mock.activated_position = parameters.get("activated-position", None) + + # When to send the Deactivated signal (in ms after Activated), 0 means no + # signal + mock.deactivated_after = parameters.get("deactivated-after", 0) + + # Position tuple for Deactivated signal, None means don't add position + mock.deactivated_position = parameters.get("deactivated-position", None) + + # When to send the Disabled signal (in ms after Enabled), 0 means no + # signal + mock.disabled_after = parameters.get("disabled-after", 0) + + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary( + { + "version": dbus.UInt32(parameters.get("version", 1)), + "SupportedCapabilities": dbus.UInt32( + parameters.get("capabilities", 0xF) + ), + } + ), + ) + + mock.active_sessions: Dict[str, Session] = {} + + +@dbus.service.method( + MAIN_IFACE, + sender_keyword="sender", + in_signature="sa{sv}", + out_signature="o", +) +def CreateSession(self, parent_window: str, options: ASVType, sender: str): + try: + request = Request(bus_name=self.bus_name, sender=sender, options=options) + session = Session(bus_name=self.bus_name, sender=sender, options=options) + + response = Response( + 0, + { + "capabilities": dbus.UInt32(0xF, variant_level=1), + "session_handle": dbus.ObjectPath(session.handle), + }, + ) + self.active_sessions[session.handle] = session + + logger.debug(f"CreateSession with response {response}") + request.respond(response, delay=self.delay) + + return request.handle + except Exception as e: + logger.critical(e) + + +@dbus.service.method( + MAIN_IFACE, + sender_keyword="sender", + in_signature="oa{sv}", + out_signature="h", +) +def ConnectToEIS(self, session_handle: str, options: ASVType, sender: str): + try: + import socket + + sockets = socket.socketpair() + # Write some random data down so it'll break anything that actually + # expects the socket to be a real EIS socket + sockets[0].send(b"VANILLA") + fd = sockets[1] + logger.debug(f"ConnectToEIS with fd {fd.fileno()}") + return dbus.types.UnixFd(fd) + except Exception as e: + logger.critical(e) + + +@dbus.service.method( + MAIN_IFACE, + sender_keyword="sender", + in_signature="oa{sv}", + out_signature="o", +) +def GetZones(self, session_handle: str, options: ASVType, sender: str): + try: + request = Request(bus_name=self.bus_name, sender=sender, options=options) + + if session_handle not in self.active_sessions: + request.respond(Response(2, {}, delay=self.delay)) + return request.handle + + zone_set = self.current_zone_set + zones = self.current_zones + + results = {} + if zone_set is not None: + results["zone_set"] = dbus.UInt32(zone_set, variant_level=1) + if zones is not None: + results["zones"] = dbus.Array( + [dbus.Struct(z, signature="uuii") for z in zones], + signature="(uuii)", + variant_level=1, + ) + + response = Response(response=0, results=results) + + logger.debug(f"GetZones with response {response}") + request.respond(response, delay=self.delay) + + if self.change_zones_after > 0: + + def change_zones(): + global zone_set + + logger.debug("Changing Zones") + opts = {"zone_set": dbus.UInt32(self.current_zone_set, variant_level=1)} + self.current_zone_set = next(zone_set) + self.current_zones = self.changed_zones + self.EmitSignalDetailed( + "", + "ZonesChanged", + "oa{sv}", + [dbus.ObjectPath(session_handle), opts], + details={"destination": sender}, + ) + + GLib.timeout_add(self.change_zones_after, change_zones) + + self.change_zones_after = 0 # Zones only change once + + return request.handle + except Exception as e: + logger.critical(e) + + +@dbus.service.method( + MAIN_IFACE, + sender_keyword="sender", + in_signature="oa{sv}aa{sv}u", + out_signature="o", +) +def SetPointerBarriers( + self, + session_handle: str, + options: ASVType, + barriers: List[ASVType], + zone_set: int, + sender: str, +): + try: + request = Request(bus_name=self.bus_name, sender=sender, options=options) + + if ( + session_handle not in self.active_sessions + or zone_set != self.current_zone_set + ): + response = Response(2, {}) + else: + results = { + "failed_barriers": dbus.Array( + self.failed_barriers, signature="u", variant_level=1 + ) + } + response = Response(0, results) + + logger.debug(f"SetPointerBarriers with response {response}") + request.respond(response, delay=self.delay) + + return request.handle + except Exception as e: + logger.critical(e) + + +@dbus.service.method( + MAIN_IFACE, + sender_keyword="sender", + in_signature="oa{sv}", + out_signature="", +) +def Enable(self, session_handle, options, sender): + try: + logger.debug(f"Enable with options {options}") + allowed_options = [] + + if not all([k in allowed_options for k in options]): + logger.error("Enable does not support options") + + if self.activated_after > 0: + + current_eis_serial = next(eis_serial) if eis_serial else None + + def send_activated(): + opts = {} + if current_eis_serial is not None: + opts["activation_id"] = dbus.UInt32( + current_eis_serial, variant_level=1 + ) + + if self.activated_position is not None: + opts["cursor_position"] = dbus.Struct( + self.activated_position, signature="dd", variant_level=1 + ) + if self.activated_barrier is not None: + opts["barrier_id"] = dbus.UInt32( + self.activated_barrier, variant_level=1 + ) + + self.EmitSignalDetailed( + "", + "Activated", + "oa{sv}", + [dbus.ObjectPath(session_handle), opts], + details={"destination": sender}, + ) + + GLib.timeout_add(self.activated_after, send_activated) + + if self.deactivated_after > 0: + + def send_deactivated(): + opts = {} + if current_eis_serial: + opts["activation_id"] = dbus.UInt32( + current_eis_serial, variant_level=1 + ) + + if self.deactivated_position is not None: + opts["cursor_position"] = dbus.Struct( + self.deactivated_position, signature="dd", variant_level=1 + ) + + self.EmitSignalDetailed( + "", + "Deactivated", + "oa{sv}", + [dbus.ObjectPath(session_handle), opts], + details={"destination": sender}, + ) + + GLib.timeout_add( + self.activated_after + self.deactivated_after, send_deactivated + ) + + if self.disabled_after > 0: + + def send_disabled(): + self.EmitSignalDetailed( + "", + "Disabled", + "oa{sv}", + [dbus.ObjectPath(session_handle), {}], + details={"destination": sender}, + ) + + GLib.timeout_add(self.disabled_after, send_disabled) + + except Exception as e: + logger.critical(e) + + +@dbus.service.method( + MAIN_IFACE, + sender_keyword="sender", + in_signature="oa{sv}", + out_signature="", +) +def Disable(self, session_handle, options, sender): + try: + logger.debug(f"Disable with options {options}") + allowed_options = [] + + if not all([k in allowed_options for k in options]): + logger.error("Disable does not support options") + except Exception as e: + logger.critical(e) + + +@dbus.service.method( + MAIN_IFACE, + sender_keyword="sender", + in_signature="oa{sv}", + out_signature="", +) +def Release(self, session_handle, options, sender): + try: + logger.debug(f"Release with options {options}") + allowed_options = ["cursor_position"] + + if not all([k in allowed_options for k in options]): + logger.error("Invalid options for Release") + except Exception as e: + logger.critical(e) diff --git a/tests/pyportaltest/test_inputcapture.py b/tests/pyportaltest/test_inputcapture.py new file mode 100644 index 00000000..9b163749 --- /dev/null +++ b/tests/pyportaltest/test_inputcapture.py @@ -0,0 +1,592 @@ +# SPDX-License-Identifier: LGPL-3.0-only +# +# This file is formatted with Python Black + +from . import PortalTest +from typing import List, Optional + +import gi +import logging +import pytest +import os + +gi.require_version("Xdp", "1.0") +from gi.repository import GLib, Gio, Xdp + +logger = logging.getLogger(f"test.{__name__}") +logger.setLevel(logging.DEBUG) + + +class SessionSetup: + def __init__( + self, + session: Xdp.InputCaptureSession = None, + zones: Optional[List[Xdp.InputCaptureZone]] = None, + barriers: Optional[List[Xdp.InputCapturePointerBarrier]] = None, + failed_barriers: Optional[List[Xdp.InputCapturePointerBarrier]] = None, + ): + self.session = session + self.zones = zones or [] + self.barriers = barriers or [] + self.failed_barriers = failed_barriers or [] + + +class SessionCreationFailed(Exception): + def __init__(self, glib_error): + self.glib_error = glib_error + + def __str__(self): + return f"SessionCreationFailed: {self.glib_error}" + + +class TestInputCapture(PortalTest): + def create_session_with_barriers( + self, + params=None, + parent=None, + capabilities=Xdp.Capability.POINTER, + barriers=None, + allow_failed_barriers=False, + cancellable=None, + ) -> SessionSetup: + """ + Session creation helper. This function creates a session and sets up + pointer barriers, with defaults for everything. + """ + params = params or {} + self.setup_daemon(params) + + xdp = Xdp.Portal.new() + assert xdp is not None + + session, session_error = None, None + create_session_done_invoked = False + + def create_session_done(portal, task, data): + nonlocal session, session_error + nonlocal create_session_done_invoked + + create_session_done_invoked = True + try: + session = portal.create_input_capture_session_finish(task) + if session is None: + session_error = Exception("XdpSession is NULL") + except GLib.GError as e: + session_error = e + self.mainloop.quit() + + xdp.create_input_capture_session( + parent=parent, + capabilities=capabilities, + cancellable=cancellable, + callback=create_session_done, + data=None, + ) + + self.mainloop.run() + assert create_session_done_invoked + if session_error is not None: + raise SessionCreationFailed(session_error) + assert session is not None + + zones = session.get_zones() + + if barriers is None: + barriers = [Xdp.InputCapturePointerBarrier(id=1, x1=0, x2=1920, y1=0, y2=0)] + + # Check that we get the notify:is-active for each barrier + active_barriers = [] + inactive_barriers = [] + + def notify_active_cb(barrier, pspec): + nonlocal active_barriers, inactive_barriers + + if barrier.props.is_active: + active_barriers.append(barrier) + else: + inactive_barriers.append(barrier) + + for b in barriers: + b.connect("notify::is-active", notify_active_cb) + + failed_barriers = None + + def set_pointer_barriers_done(session, task, data): + nonlocal session_error, failed_barriers + nonlocal set_pointer_barriers_done_invoked + + set_pointer_barriers_done_invoked = True + try: + failed_barriers = session.set_pointer_barriers_finish(task) + except GLib.GError as e: + session_error = e + self.mainloop.quit() + + set_pointer_barriers_done_invoked = False + session.set_pointer_barriers( + barriers=barriers, + cancellable=None, + callback=set_pointer_barriers_done, + data=None, + ) + self.mainloop.run() + + if session_error is not None: + raise SessionCreationFailed(session_error) + + assert set_pointer_barriers_done_invoked + assert sorted(active_barriers + inactive_barriers) == sorted(barriers) + + if not allow_failed_barriers: + assert ( + failed_barriers == [] + ), "Barriers failed but allow_failed_barriers was not set" + + return SessionSetup( + session=session, + zones=zones, + barriers=active_barriers, + failed_barriers=failed_barriers, + ) + + def test_version(self): + """This tests the test suite setup rather than libportal""" + params = {} + self.setup_daemon(params) + assert self.properties_interface.Get(self.INTERFACE_NAME, "version") == 1 + + def test_session_create(self): + """ + The basic test of successful create and zone check + """ + params = { + "zones": [(1920, 1080, 0, 0), (1080, 1920, 1920, 1080)], + "zone-set": 1234, + } + self.setup_daemon(params) + + capabilities = Xdp.Capability.POINTER | Xdp.Capability.KEYBOARD + + setup = self.create_session_with_barriers(params, capabilities=capabilities) + assert setup.session is not None + zones = setup.zones + assert len(zones) == 2 + z1 = zones[0] + assert z1.props.width == 1920 + assert z1.props.height == 1080 + assert z1.props.x == 0 + assert z1.props.y == 0 + assert z1.props.zone_set == 1234 + + z2 = zones[1] + assert z2.props.width == 1080 + assert z2.props.height == 1920 + assert z2.props.x == 1920 + assert z2.props.y == 1080 + assert z2.props.zone_set == 1234 + + # Now verify our DBus calls were correct + method_calls = self.mock_interface.GetMethodCalls("CreateSession") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + parent, options = args + assert list(options.keys()) == [ + "handle_token", + "session_handle_token", + "capabilities", + ] + assert options["capabilities"] == capabilities + + method_calls = self.mock_interface.GetMethodCalls("GetZones") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + session_handle, options = args + assert list(options.keys()) == ["handle_token"] + + def test_session_create_cancel_during_create(self): + """ + Create a session but cancel while waiting for the CreateSession request + """ + params = {"delay": 1000} + self.setup_daemon(params) + cancellable = Gio.Cancellable() + GLib.timeout_add(300, cancellable.cancel) + + with pytest.raises(SessionCreationFailed) as e: + self.create_session_with_barriers(params=params, cancellable=cancellable) + assert "Operation was cancelled" in e.glib_error.message + + def test_session_create_cancel_during_getzones(self): + """ + Create a session but cancel while waiting for the GetZones request + """ + # libportal issues two requests: CreateSession and GetZones, + # param is set for each to delay 500 ms so if we cancel after 700, the + # one that is cancelled should be the GetZones one. + # Can't guarantee it but this is the best we can do + params = {"delay": 500} + self.setup_daemon(params) + cancellable = Gio.Cancellable() + GLib.timeout_add(700, cancellable.cancel) + + with pytest.raises(SessionCreationFailed) as e: + self.create_session_with_barriers(params=params, cancellable=cancellable) + assert "Operation was cancelled" in e.glib_error.message + + def test_session_create_no_serial_on_getzones(self): + """ + Test buggy portal implementation not replying with a zone_set in + GetZones + """ + params = { + "zone-set": -1, + } + + self.setup_daemon(params) + with pytest.raises(SessionCreationFailed): + self.create_session_with_barriers(params) + + def test_session_create_no_zones_on_getzones(self): + """ + Test buggy portal implementation not replying with a zone + GetZones + """ + params = { + "zones": [(0, 0, 0, 0)], + } + + self.setup_daemon(params) + with pytest.raises(SessionCreationFailed): + self.create_session_with_barriers(params) + + def _test_session_create_without_subref(self): + """ + Create a new InputCapture session but never access the actual + input capture session. + """ + self.setup_daemon({}) + + xdp = Xdp.Portal.new() + assert xdp is not None + + parent_session, session_error = None, None + create_session_done_invoked = False + + def create_session_done(portal, task, data): + nonlocal parent_session, session_error + nonlocal create_session_done_invoked + + create_session_done_invoked = True + try: + parent_session = portal.create_input_capture_session_finish(task) + if parent_session is None: + session_error = Exception("XdpSession is NULL") + except GLib.GError as e: + session_error = e + self.mainloop.quit() + + capabilities = Xdp.Capability.POINTER | Xdp.Capability.KEYBOARD + xdp.create_input_capture_session( + parent=None, + capabilities=capabilities, + cancellable=None, + callback=create_session_done, + data=None, + ) + + self.mainloop.run() + assert create_session_done_invoked + + # Explicitly don't call parent_session.get_input_capture_session() + # since that would cause python to g_object_ref the IC session. + # By not doing so we never ref that object and can test for the correct + # cleanup + + def test_connect_to_eis(self): + """ + The basic test of retrieving the EIS handle + """ + params = {} + self.setup_daemon(params) + setup = self.create_session_with_barriers(params) + assert setup.session is not None + + handle = setup.session.connect_to_eis() + assert handle >= 0 + + fd = os.fdopen(handle) + buf = fd.read() + assert buf == "VANILLA" # template sends this by default + + # Now verify our DBus calls were correct + method_calls = self.mock_interface.GetMethodCalls("ConnectToEIS") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + parent, options = args + assert "handle_token" not in options # This is not a Request + assert list(options.keys()) == [] + + def test_pointer_barriers_success(self): + """ + Some successful pointer barriers + """ + b1 = Xdp.InputCapturePointerBarrier(id=1, x1=0, x2=1920, y1=0, y2=0) + b2 = Xdp.InputCapturePointerBarrier(id=2, x1=1920, x2=1920, y1=0, y2=1080) + + params = {} + self.setup_daemon(params) + setup = self.create_session_with_barriers(params, barriers=[b1, b2]) + assert setup.barriers == [b1, b2] + + # Now verify our DBus calls were correct + method_calls = self.mock_interface.GetMethodCalls("SetPointerBarriers") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + session_handle, options, barriers, zone_set = args + assert list(options.keys()) == ["handle_token"] + for b in barriers: + assert "barrier_id" in b + assert "position" in b + assert b["barrier_id"] in [1, 2] + x1, y1, x2, y2 = [int(x) for x in b["position"]] + if b["barrier_id"] == 1: + assert (x1, y1, x2, y2) == (0, 0, 1920, 0) + if b["barrier_id"] == 2: + assert (x1, y1, x2, y2) == (1920, 0, 1920, 1080) + + def test_pointer_barriers_failures(self): + """ + Test with some barriers failing + """ + b1 = Xdp.InputCapturePointerBarrier(id=1, x1=0, x2=1920, y1=0, y2=0) + b2 = Xdp.InputCapturePointerBarrier(id=2, x1=1, x2=2, y1=3, y2=4) + b3 = Xdp.InputCapturePointerBarrier(id=3, x1=1, x2=2, y1=3, y2=4) + b4 = Xdp.InputCapturePointerBarrier(id=4, x1=1920, x2=1920, y1=0, y2=1080) + + params = {"failed-barriers": [2, 3]} + self.setup_daemon(params) + setup = self.create_session_with_barriers( + params, barriers=[b1, b2, b3, b4], allow_failed_barriers=True + ) + assert setup.barriers == [b1, b4] + assert setup.failed_barriers == [b2, b3] + + # Now verify our DBus calls were correct + method_calls = self.mock_interface.GetMethodCalls("SetPointerBarriers") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + session_handle, options, barriers, zone_set = args + assert list(options.keys()) == ["handle_token"] + for b in barriers: + assert "barrier_id" in b + assert "position" in b + assert b["barrier_id"] in [1, 2, 3, 4] + x1, y1, x2, y2 = [int(x) for x in b["position"]] + if b["barrier_id"] == 1: + assert (x1, y1, x2, y2) == (0, 0, 1920, 0) + if b["barrier_id"] in [2, 3]: + assert (x1, y1, x2, y2) == (1, 3, 2, 4) + if b["barrier_id"] == 4: + assert (x1, y1, x2, y2) == (1920, 0, 1920, 1080) + + def test_enable_disable_release(self): + """ + Test enable/disable calls + """ + params = {} + self.setup_daemon(params) + + setup = self.create_session_with_barriers(params) + session = setup.session + + session.enable() + session.disable() + session.release(activation_id=456) # fake id, doesn't matter here + + self.mainloop.run() + + # Now verify our DBus calls were correct + method_calls = self.mock_interface.GetMethodCalls("Enable") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + session_handle, options = args + assert list(options.keys()) == [] + + method_calls = self.mock_interface.GetMethodCalls("Disable") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + session_handle, options = args + assert list(options.keys()) == [] + + method_calls = self.mock_interface.GetMethodCalls("Release") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + session_handle, options = args + assert list(options.keys()) == ["activation_id"] + + def test_release_at(self): + """ + Test the release_at call with a cursor position + """ + params = {} + self.setup_daemon(params) + + setup = self.create_session_with_barriers(params) + session = setup.session + + # libportal allows us to call Release without Enable first, + # we just fake an activation_id + session.release_at( + activation_id=456, cursor_x_position=10, cursor_y_position=10 + ) + self.mainloop.run() + + # Now verify our DBus calls were correct + method_calls = self.mock_interface.GetMethodCalls("Release") + assert len(method_calls) == 1 + _, args = method_calls.pop(0) + session_handle, options = args + assert list(options.keys()) == ["activation_id", "cursor_position"] + cursor_position = options["cursor_position"] + assert cursor_position == (10.0, 10.0) + + def test_activated(self): + """ + Test the Activated signal + """ + params = { + "eis-serial": 123, + "activated-after": 20, + "activated-barrier": 1, + "activated-position": (10.0, 20.0), + "deactivated-after": 20, + "deactivated-position": (20.0, 30.0), + } + self.setup_daemon(params) + + setup = self.create_session_with_barriers(params) + session = setup.session + + session_activated_signal_received = False + session_deactivated_signal_received = False + signal_activated_options = None + signal_deactivated_options = None + signal_activation_id = None + signal_deactivation_id = None + + def session_activated(session, activation_id, opts): + nonlocal session_activated_signal_received + nonlocal signal_activation_id, signal_activated_options + session_activated_signal_received = True + signal_activated_options = opts + signal_activation_id = activation_id + + def session_deactivated(session, activation_id, opts): + nonlocal session_deactivated_signal_received + nonlocal signal_deactivation_id, signal_deactivated_options + session_deactivated_signal_received = True + signal_deactivated_options = opts + signal_deactivation_id = activation_id + self.mainloop.quit() + + session.connect("activated", session_activated) + session.connect("deactivated", session_deactivated) + session.enable() + + self.mainloop.run() + + assert session_activated_signal_received + assert signal_activated_options is not None + assert signal_activation_id == 123 + assert list(signal_activated_options.keys()) == [ + "activation_id", + "cursor_position", + "barrier_id", + ] + assert signal_activated_options["barrier_id"] == 1 + assert signal_activated_options["cursor_position"] == (10.0, 20.0) + assert signal_activated_options["activation_id"] == 123 + + assert session_deactivated_signal_received + assert signal_deactivated_options is not None + assert signal_deactivation_id == 123 + assert list(signal_deactivated_options.keys()) == [ + "activation_id", + "cursor_position", + ] + assert signal_deactivated_options["cursor_position"] == (20.0, 30.0) + assert signal_deactivated_options["activation_id"] == 123 + + def test_zones_changed(self): + """ + Test the ZonesChanged signal + """ + params = { + "zones": [(1920, 1080, 0, 0), (1080, 1920, 1920, 1080)], + "changed-zones": [(1024, 768, 0, 0)], + "change-zones-after": 200, + "zone-set": 567, + } + self.setup_daemon(params) + + setup = self.create_session_with_barriers(params) + session = setup.session + + signal_received = False + signal_options = None + zone_props = {z: None for z in setup.zones} + + def zones_changed(session, opts): + nonlocal signal_received, signal_options, zone_props + signal_received = True + signal_options = opts + if signal_received and all([v == False for v in zone_props.values()]): + self.mainloop.quit() + + session.connect("zones-changed", zones_changed) + + def zones_is_valid_changed(zone, pspec): + nonlocal zone_props, signal_received + zone_props[zone] = zone.props.is_valid + if signal_received and all([v == False for v in zone_props.values()]): + self.mainloop.quit() + + for z in setup.zones: + z.connect("notify::is-valid", zones_is_valid_changed) + + self.mainloop.run() + + assert signal_received + assert signal_options is not None + assert list(signal_options.keys()) == ["zone_set"] + assert signal_options["zone_set"] == 567 + + assert all([z.props.zone_set == 568 for z in session.get_zones()]) + assert all([v == False for v in zone_props.values()]) + + def test_disabled(self): + """ + Test the Disabled signal + """ + params = { + "disabled-after": 20, + } + self.setup_daemon(params) + + setup = self.create_session_with_barriers(params) + session = setup.session + + disabled_signal_received = False + + def session_disabled(session, options): + nonlocal disabled_signal_received + disabled_signal_received = True + self.mainloop.quit() + + session.connect("disabled", session_disabled) + + session.enable() + + self.mainloop.run() + + assert disabled_signal_received