diff --git a/dev-libs/libportal/Manifest b/dev-libs/libportal/Manifest new file mode 100644 index 0000000..26fb144 --- /dev/null +++ b/dev-libs/libportal/Manifest @@ -0,0 +1 @@ +DIST libportal-0.7.1.tar.xz 74268 BLAKE2B b519fa88735d640a74e18cc791ec69862f136b793a7c855b1f3873cf6b15626d69088747f1a7ff54f8cd96f79e82e3df31e5349e3da57906e769b8f809f4ba34 SHA512 cbc50bfd86787fffc975fc53835acc6c3c0fd54b7ee02fce1983f1bd0fc40b15a0537780cd5e943ecedcf951840080a0f55a23a96e706223e52a6144ee70332c diff --git a/dev-libs/libportal/files/0001-Input-capture-support.patch b/dev-libs/libportal/files/0001-Input-capture-support.patch new file mode 100644 index 0000000..304ed50 --- /dev/null +++ b/dev-libs/libportal/files/0001-Input-capture-support.patch @@ -0,0 +1,3889 @@ +From 4abbe42691dab79a17ff6a8ddfe5f17d2688a750 Mon Sep 17 00:00:00 2001 +From: Peter Hutterer +Date: Tue, 29 Mar 2022 11:05:47 +1000 +Subject: [PATCH] Input capture support. + +--- + libportal/inputcapture-pointerbarrier.c | 248 ++++ + libportal/inputcapture-pointerbarrier.h | 31 + + libportal/inputcapture-private.h | 30 + + libportal/inputcapture-zone.c | 238 ++++ + libportal/inputcapture-zone.h | 31 + + libportal/inputcapture.c | 1203 +++++++++++++++++ + libportal/inputcapture.h | 103 ++ + libportal/meson.build | 7 + + libportal/portal.h | 2 + + libportal/remote.c | 134 +- + libportal/remote.h | 56 +- + libportal/session-private.h | 12 +- + libportal/session.c | 130 +- + libportal/session.h | 51 + + portal-test/gtk3/portal-test-win.c | 102 +- + portal-test/gtk3/portal-test-win.ui | 62 + + tests/pyportaltest/templates/__init__.py | 2 +- + tests/pyportaltest/templates/inputcapture.py | 372 +++++ + tests/pyportaltest/templates/remotedesktop.py | 7 +- + tests/pyportaltest/test_inputcapture.py | 646 +++++++++ + tests/pyportaltest/test_remotedesktop.py | 22 + + 21 files changed, 3315 insertions(+), 174 deletions(-) + create mode 100644 libportal/inputcapture-pointerbarrier.c + create mode 100644 libportal/inputcapture-pointerbarrier.h + create mode 100644 libportal/inputcapture-private.h + create mode 100644 libportal/inputcapture-zone.c + create mode 100644 libportal/inputcapture-zone.h + create mode 100644 libportal/inputcapture.c + create mode 100644 libportal/inputcapture.h + create mode 100644 libportal/session.h + create mode 100644 tests/pyportaltest/templates/inputcapture.py + create mode 100644 tests/pyportaltest/test_inputcapture.py + +diff --git a/libportal/inputcapture-pointerbarrier.c b/libportal/inputcapture-pointerbarrier.c +new file mode 100644 +index 0000000..d904a6c +--- /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 0000000..52db9bb +--- /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 0000000..e554df2 +--- /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 0000000..7b27ede +--- /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 0000000..88bfb51 +--- /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 0000000..d20eeed +--- /dev/null ++++ b/libportal/inputcapture.c +@@ -0,0 +1,1203 @@ ++/* ++ * 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) ++ { ++ 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 (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_uint32 (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 0000000..fff9468 +--- /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 35cf616..792b2bf 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', +@@ -19,6 +22,7 @@ headers = [ + 'print.h', + 'remote.h', + 'screenshot.h', ++ 'session.h', + 'spawn.h', + 'trash.h', + 'types.h', +@@ -43,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 bc7a09b..3618b81 100644 +--- a/libportal/portal.h ++++ b/libportal/portal.h +@@ -27,6 +27,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -34,6 +35,7 @@ + #include + #include + #include ++#include + #include + #include + #include +diff --git a/libportal/remote.c b/libportal/remote.c +index ce77927..ef69d12 100644 +--- a/libportal/remote.c ++++ b/libportal/remote.c +@@ -826,29 +826,6 @@ xdp_session_start_finish (XdpSession *session, + return g_task_propagate_boolean (G_TASK (result), error); + } + +-/** +- * xdp_session_close: +- * @session: an active [class@Session] +- * +- * Closes the session. +- */ +-void +-xdp_session_close (XdpSession *session) +-{ +- g_return_if_fail (XDP_IS_SESSION (session)); +- +- g_dbus_connection_call (session->portal->bus, +- PORTAL_BUS_NAME, +- session->id, +- SESSION_INTERFACE, +- "Close", +- NULL, +- NULL, 0, -1, NULL, NULL, NULL); +- +- _xdp_session_set_session_state (session, XDP_SESSION_CLOSED); +- g_signal_emit_by_name (session, "closed"); +-} +- + /** + * xdp_session_open_pipewire_remote: + * @session: a [class@Session] +@@ -1319,3 +1296,114 @@ xdp_session_get_restore_token (XdpSession *session) + + return g_strdup (session->restore_token); + } ++ ++/** ++ * xdp_session_get_devices: ++ * @session: a [class@Session] ++ * ++ * Obtains the devices that the user selected. ++ * ++ * Unless the session is active, this function returns `XDP_DEVICE_NONE`. ++ * ++ * Returns: the selected devices ++ */ ++XdpDeviceType ++xdp_session_get_devices (XdpSession *session) ++{ ++ g_return_val_if_fail (XDP_IS_SESSION (session), XDP_DEVICE_NONE); ++ ++ if (session->state != XDP_SESSION_ACTIVE) ++ return XDP_DEVICE_NONE; ++ ++ return session->devices; ++} ++ ++void ++_xdp_session_set_devices (XdpSession *session, ++ XdpDeviceType devices) ++{ ++ session->devices = devices; ++} ++ ++/** ++ * xdp_session_get_streams: ++ * @session: a [class@Session] ++ * ++ * Obtains the streams that the user selected. ++ * ++ * The information in the returned [struct@GLib.Variant] has the format ++ * `a(ua{sv})`. Each item in the array is describing a stream. The first member ++ * is the pipewire node ID, the second is a dictionary of stream properties, ++ * including: ++ * ++ * - position, `(ii)`: a tuple consisting of the position `(x, y)` in the compositor ++ * coordinate space. Note that the position may not be equivalent to a ++ * position in a pixel coordinate space. Only available for monitor streams. ++ * - size, `(ii)`: a tuple consisting of (width, height). The size represents the size ++ * of the stream as it is displayed in the compositor coordinate space. ++ * Note that this size may not be equivalent to a size in a pixel coordinate ++ * space. The size may differ from the size of the stream. ++ * ++ * Unless the session is active, this function returns `NULL`. ++ * ++ * Returns: the selected streams ++ */ ++GVariant * ++xdp_session_get_streams (XdpSession *session) ++{ ++ g_return_val_if_fail (XDP_IS_SESSION (session), NULL); ++ ++ if (session->state != XDP_SESSION_ACTIVE) ++ return NULL; ++ ++ return session->streams; ++} ++ ++void ++_xdp_session_set_streams (XdpSession *session, ++ GVariant *streams) ++{ ++ if (session->streams) ++ g_variant_unref (session->streams); ++ session->streams = streams; ++ if (session->streams) ++ g_variant_ref (session->streams); ++} ++ ++/** ++ * xdp_session_get_session_state: ++ * @session: an [class@Session] ++ * ++ * Obtains information about the state of the session that is represented ++ * by @session. ++ * ++ * Returns: the state of @session ++ */ ++XdpSessionState ++xdp_session_get_session_state (XdpSession *session) ++{ ++ g_return_val_if_fail (XDP_IS_SESSION (session), XDP_SESSION_CLOSED); ++ ++ return session->state; ++} ++ ++void ++_xdp_session_set_session_state (XdpSession *session, ++ XdpSessionState state) ++{ ++ session->state = state; ++ ++ if (state == XDP_SESSION_INITIAL && session->state != XDP_SESSION_INITIAL) ++ { ++ g_warning ("Can't move a session back to initial state"); ++ return; ++ } ++ if (session->state == XDP_SESSION_CLOSED && state != XDP_SESSION_CLOSED) ++ { ++ g_warning ("Can't move a session back from closed state"); ++ return; ++ } ++ ++ if (state == XDP_SESSION_CLOSED) ++ _xdp_session_close (session); ++} +diff --git a/libportal/remote.h b/libportal/remote.h +index a751861..b466e4b 100644 +--- a/libportal/remote.h ++++ b/libportal/remote.h +@@ -20,13 +20,23 @@ + #pragma once + + #include ++#include + + G_BEGIN_DECLS + +-#define XDP_TYPE_SESSION (xdp_session_get_type ()) +- +-XDP_PUBLIC +-G_DECLARE_FINAL_TYPE (XdpSession, xdp_session, XDP, SESSION, GObject) ++/** ++ * XdpSessionState: ++ * @XDP_SESSION_INITIAL: the session has not been started. ++ * @XDP_SESSION_ACTIVE: the session is active. ++ * @XDP_SESSION_CLOSED: the session is no longer active. ++ * ++ * The state of a session. ++ */ ++typedef enum { ++ XDP_SESSION_INITIAL, ++ XDP_SESSION_ACTIVE, ++ XDP_SESSION_CLOSED ++} XdpSessionState; + + /** + * XdpOutputType: +@@ -60,32 +70,6 @@ typedef enum { + XDP_DEVICE_TOUCHSCREEN = 1 << 2 + } XdpDeviceType; + +-/** +- * XdpSessionType: +- * @XDP_SESSION_SCREENCAST: a screencast session. +- * @XDP_SESSION_REMOTE_DESKTOP: a remote desktop session. +- * +- * The type of a session. +- */ +-typedef enum { +- XDP_SESSION_SCREENCAST, +- XDP_SESSION_REMOTE_DESKTOP +-} XdpSessionType; +- +-/** +- * XdpSessionState: +- * @XDP_SESSION_INITIAL: the session has not been started. +- * @XDP_SESSION_ACTIVE: the session is active. +- * @XDP_SESSION_CLOSED: the session is no longer active. +- * +- * The state of a session. +- */ +-typedef enum { +- XDP_SESSION_INITIAL, +- XDP_SESSION_ACTIVE, +- XDP_SESSION_CLOSED +-} XdpSessionState; +- + /** + * XdpScreencastFlags: + * @XDP_SCREENCAST_FLAG_NONE: No options +@@ -169,6 +153,9 @@ XdpSession *xdp_portal_create_remote_desktop_session_finish (XdpPortal + GAsyncResult *result, + GError **error); + ++XDP_PUBLIC ++XdpSessionState xdp_session_get_session_state (XdpSession *session); ++ + XDP_PUBLIC + void xdp_session_start (XdpSession *session, + XdpParent *parent, +@@ -181,18 +168,9 @@ gboolean xdp_session_start_finish (XdpSession *session, + GAsyncResult *result, + GError **error); + +-XDP_PUBLIC +-void xdp_session_close (XdpSession *session); +- + XDP_PUBLIC + int xdp_session_open_pipewire_remote (XdpSession *session); + +-XDP_PUBLIC +-XdpSessionType xdp_session_get_session_type (XdpSession *session); +- +-XDP_PUBLIC +-XdpSessionState xdp_session_get_session_state (XdpSession *session); +- + XDP_PUBLIC + XdpDeviceType xdp_session_get_devices (XdpSession *session); + +diff --git a/libportal/session-private.h b/libportal/session-private.h +index c21661b..c452520 100644 +--- a/libportal/session-private.h ++++ b/libportal/session-private.h +@@ -20,22 +20,30 @@ + #pragma once + + #include ++#include + + struct _XdpSession { + GObject parent_instance; + ++ /* Generic Session implementation */ + XdpPortal *portal; + char *id; ++ gboolean is_closed; + XdpSessionType type; ++ guint signal_id; ++ ++ /* RemoteDesktop/ScreenCast */ + XdpSessionState state; + XdpDeviceType devices; + GVariant *streams; + + XdpPersistMode persist_mode; + char *restore_token; ++ + gboolean uses_eis; + +- guint signal_id; ++ /* InputCapture */ ++ XdpInputCaptureSession *input_capture_session; /* weak ref */ + }; + + XdpSession * _xdp_session_new (XdpPortal *portal, +@@ -50,3 +58,5 @@ void _xdp_session_set_devices (XdpSession *session, + + void _xdp_session_set_streams (XdpSession *session, + GVariant *streams); ++ ++void _xdp_session_close (XdpSession *session); +diff --git a/libportal/session.c b/libportal/session.c +index 0b1f02a..a068851 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, +@@ -129,6 +133,16 @@ _xdp_session_new (XdpPortal *portal, + return session; + } + ++void ++_xdp_session_close (XdpSession *session) ++{ ++ if (session->is_closed) ++ return; ++ ++ session->is_closed = TRUE; ++ g_signal_emit_by_name (session, "closed"); ++} ++ + /** + * xdp_session_get_session_type: + * @session: an [class@Session] +@@ -147,112 +161,24 @@ xdp_session_get_session_type (XdpSession *session) + } + + /** +- * xdp_session_get_session_state: +- * @session: an [class@Session] ++ * xdp_session_close: ++ * @session: an active [class@Session] + * +- * Obtains information about the state of the session that is represented +- * by @session. +- * +- * Returns: the state of @session ++ * Closes the session. + */ +-XdpSessionState +-xdp_session_get_session_state (XdpSession *session) +-{ +- g_return_val_if_fail (XDP_IS_SESSION (session), XDP_SESSION_CLOSED); +- +- return session->state; +-} +- + void +-_xdp_session_set_session_state (XdpSession *session, +- XdpSessionState state) +-{ +- session->state = state; +- +- if (state == XDP_SESSION_INITIAL && session->state != XDP_SESSION_INITIAL) +- { +- g_warning ("Can't move a session back to initial state"); +- return; +- } +- if (session->state == XDP_SESSION_CLOSED && state != XDP_SESSION_CLOSED) +- { +- g_warning ("Can't move a session back from closed state"); +- return; +- } +- +- if (state == XDP_SESSION_CLOSED) +- g_signal_emit (session, signals[CLOSED], 0); +-} +- +-/** +- * xdp_session_get_devices: +- * @session: a [class@Session] +- * +- * Obtains the devices that the user selected. +- * +- * Unless the session is active, this function returns `XDP_DEVICE_NONE`. +- * +- * Returns: the selected devices +- */ +-XdpDeviceType +-xdp_session_get_devices (XdpSession *session) ++xdp_session_close (XdpSession *session) + { +- g_return_val_if_fail (XDP_IS_SESSION (session), XDP_DEVICE_NONE); ++ g_return_if_fail (XDP_IS_SESSION (session)); + +- if (session->state != XDP_SESSION_ACTIVE) +- return XDP_DEVICE_NONE; ++ g_dbus_connection_call (session->portal->bus, ++ PORTAL_BUS_NAME, ++ session->id, ++ SESSION_INTERFACE, ++ "Close", ++ NULL, ++ NULL, 0, -1, NULL, NULL, NULL); + +- return session->devices; +-} +- +-void +-_xdp_session_set_devices (XdpSession *session, +- XdpDeviceType devices) +-{ +- session->devices = devices; +-} +- +-/** +- * xdp_session_get_streams: +- * @session: a [class@Session] +- * +- * Obtains the streams that the user selected. +- * +- * The information in the returned [struct@GLib.Variant] has the format +- * `a(ua{sv})`. Each item in the array is describing a stream. The first member +- * is the pipewire node ID, the second is a dictionary of stream properties, +- * including: +- * +- * - position, `(ii)`: a tuple consisting of the position `(x, y)` in the compositor +- * coordinate space. Note that the position may not be equivalent to a +- * position in a pixel coordinate space. Only available for monitor streams. +- * - size, `(ii)`: a tuple consisting of (width, height). The size represents the size +- * of the stream as it is displayed in the compositor coordinate space. +- * Note that this size may not be equivalent to a size in a pixel coordinate +- * space. The size may differ from the size of the stream. +- * +- * Unless the session is active, this function returns `NULL`. +- * +- * Returns: the selected streams +- */ +-GVariant * +-xdp_session_get_streams (XdpSession *session) +-{ +- g_return_val_if_fail (XDP_IS_SESSION (session), NULL); +- +- if (session->state != XDP_SESSION_ACTIVE) +- return NULL; +- +- return session->streams; +-} +- +-void +-_xdp_session_set_streams (XdpSession *session, +- GVariant *streams) +-{ +- if (session->streams) +- g_variant_unref (session->streams); +- session->streams = streams; +- if (session->streams) +- g_variant_ref (session->streams); ++ _xdp_session_set_session_state (session, XDP_SESSION_CLOSED); ++ _xdp_session_close (session); + } +diff --git a/libportal/session.h b/libportal/session.h +new file mode 100644 +index 0000000..e9f0214 +--- /dev/null ++++ b/libportal/session.h +@@ -0,0 +1,51 @@ ++/* ++ * 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 ++ ++G_BEGIN_DECLS ++ ++#define XDP_TYPE_SESSION (xdp_session_get_type ()) ++ ++XDP_PUBLIC ++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 ++void xdp_session_close (XdpSession *session); ++ ++XDP_PUBLIC ++XdpSessionType xdp_session_get_session_type (XdpSession *session); ++ ++G_END_DECLS +diff --git a/portal-test/gtk3/portal-test-win.c b/portal-test/gtk3/portal-test-win.c +index 74b0fef..eeff9df 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 7112a19..e449c29 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/__init__.py b/tests/pyportaltest/templates/__init__.py +index dc8f3ac..d74c92a 100644 +--- a/tests/pyportaltest/templates/__init__.py ++++ b/tests/pyportaltest/templates/__init__.py +@@ -113,7 +113,7 @@ class Session: + def respond(): + logger.debug(f"Session.Closed on {self.handle}: {details}") + self.mock.EmitSignalDetailed( +- "", "Closed", "a{sv}", [details], destination=self.sender ++ "", "Closed", "a{sv}", [details], details={"destination": self.sender} + ) + + if delay > 0: +diff --git a/tests/pyportaltest/templates/inputcapture.py b/tests/pyportaltest/templates/inputcapture.py +new file mode 100644 +index 0000000..2cd0b32 +--- /dev/null ++++ b/tests/pyportaltest/templates/inputcapture.py +@@ -0,0 +1,372 @@ ++# 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={}): ++ 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) ++ ++ # How many ms to signal Session.Closed after Start ++ mock.close_after_enable = parameters.get("close-after-enable", 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) ++ ++ if self.close_after_enable > 0: ++ session = self.active_sessions[session_handle] ++ session.close({}, self.close_after_enable) ++ ++ 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/templates/remotedesktop.py b/tests/pyportaltest/templates/remotedesktop.py +index ebf0340..f418938 100644 +--- a/tests/pyportaltest/templates/remotedesktop.py ++++ b/tests/pyportaltest/templates/remotedesktop.py +@@ -22,7 +22,7 @@ _restore_tokens = count() + + + def load(mock, parameters): +- logger.debug(f"loading {MAIN_IFACE} template") ++ logger.debug(f"loading {MAIN_IFACE} template with params {parameters}") + + params = MockParams.get(mock, MAIN_IFACE) + params.delay = 500 +@@ -30,6 +30,7 @@ def load(mock, parameters): + params.response = parameters.get("response", 0) + params.devices = parameters.get("devices", 0b111) + params.sessions: Dict[str, Session] = {} ++ params.close_after_start = parameters.get("close-after-start", 0) + + mock.AddProperties( + MAIN_IFACE, +@@ -108,6 +109,10 @@ def Start(self, session_handle, parent_window, options, sender): + + request.respond(response, delay=params.delay) + ++ if params.close_after_start > 0: ++ session = params.sessions[session_handle] ++ session.close({}, params.close_after_start) ++ + return request.handle + 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 0000000..245031c +--- /dev/null ++++ b/tests/pyportaltest/test_inputcapture.py +@@ -0,0 +1,646 @@ ++# 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, ++ session_handle_token: Optional[str] = None, ++ ): ++ self.session = session ++ self.zones = zones or [] ++ self.barriers = barriers or [] ++ self.failed_barriers = failed_barriers or [] ++ self.session_handle_token = session_handle_token ++ ++ ++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.InputCapability.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 ++ assert session.get_session().get_session_type() == Xdp.SessionType.INPUT_CAPTURE ++ ++ # Extract our expected session id. This isn't available from ++ # XdpSession so we need to go around it. We can't easily get the ++ # sender id so the full path is hard. Let's just extract the token and ++ # pretend that's good enough. ++ method_calls = self.mock_interface.GetMethodCalls("CreateSession") ++ assert len(method_calls) >= 1 ++ _, args = method_calls.pop() # Assume the latest has our session ++ (_, options) = args ++ session_handle = options["session_handle_token"] ++ ++ 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, ++ session_handle_token=session_handle, ++ ) ++ ++ 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, ++ } ++ ++ capabilities = Xdp.InputCapability.POINTER | Xdp.InputCapability.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} ++ 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} ++ 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, ++ } ++ ++ 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)], ++ } ++ ++ 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.InputCapability.POINTER | Xdp.InputCapability.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 = {} ++ 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 = {} ++ 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]} ++ 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 = {} ++ ++ 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 = {} ++ ++ 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), ++ } ++ ++ 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, ++ } ++ ++ 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, ++ } ++ ++ 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 ++ ++ def test_close_session(self): ++ """ ++ Ensure that closing our session explicitly closes the session on DBus. ++ """ ++ setup = self.create_session_with_barriers() ++ session = setup.session ++ xdp_session = setup.session.get_session() ++ ++ was_closed = False ++ ++ def method_called(method_name, method_args, path): ++ nonlocal was_closed ++ ++ if method_name == "Close" and path.endswith(setup.session_handle_token): ++ was_closed = True ++ self.mainloop.quit() ++ ++ bus = self.get_dbus() ++ bus.add_signal_receiver( ++ handler_function=method_called, ++ signal_name="MethodCalled", ++ dbus_interface="org.freedesktop.DBus.Mock", ++ path_keyword="path", ++ ) ++ ++ xdp_session.close() ++ self.mainloop.run() ++ ++ assert was_closed is True ++ ++ def test_close_session_signal(self): ++ """ ++ Ensure that we get the GObject signal when our session is closed ++ externally. ++ """ ++ params = {"close-after-enable": 500} ++ setup = self.create_session_with_barriers(params) ++ session = setup.session ++ xdp_session = setup.session.get_session() ++ ++ session_closed_signal_received = False ++ ++ def session_closed(session): ++ nonlocal session_closed_signal_received ++ session_closed_signal_received = True ++ ++ xdp_session.connect("closed", session_closed) ++ ++ session.enable() ++ self.mainloop.run() ++ ++ assert session_closed_signal_received is True +diff --git a/tests/pyportaltest/test_remotedesktop.py b/tests/pyportaltest/test_remotedesktop.py +index 4250141..bb36db4 100644 +--- a/tests/pyportaltest/test_remotedesktop.py ++++ b/tests/pyportaltest/test_remotedesktop.py +@@ -490,3 +490,25 @@ class TestRemoteDesktop(PortalTest): + self.mainloop.run() + + assert was_closed is True ++ ++ def test_close_session_signal(self): ++ """ ++ Ensure that we get the GObject signal when our session is closed ++ externally. ++ """ ++ params = {"close-after-start": 500} ++ setup = self.create_session(params=params) ++ session = setup.session ++ ++ session_closed_signal_received = False ++ ++ def session_closed(session): ++ nonlocal session_closed_signal_received ++ session_closed_signal_received = True ++ self.mainloop.quit() ++ ++ session.connect("closed", session_closed) ++ ++ self.mainloop.run() ++ ++ assert session_closed_signal_received is True +-- +2.44.2 + diff --git a/dev-libs/libportal/files/6cd7c2ab82575b76f876ee2bd2d31f6cb77f022f.patch b/dev-libs/libportal/files/6cd7c2ab82575b76f876ee2bd2d31f6cb77f022f.patch new file mode 100644 index 0000000..ea055c8 --- /dev/null +++ b/dev-libs/libportal/files/6cd7c2ab82575b76f876ee2bd2d31f6cb77f022f.patch @@ -0,0 +1,47 @@ +From 6cd7c2ab82575b76f876ee2bd2d31f6cb77f022f Mon Sep 17 00:00:00 2001 +From: Simon McVittie +Date: Tue, 26 Dec 2023 14:35:46 +0000 +Subject: [PATCH] pyportaltest: Only create one session bus per DBusTestCase + subclass + +DBusTestCase.start_session_bus() is a class method, and can only be +called once per class, because DBusTestCase.tearDownClass() will only +clean up one session bus. In older versions of dbusmock, calling it more +than once will result in dbus-daemon processes being leaked; since +0.30.0, calling it more than once will result in an assertion failure. + +Resolves: https://github.com/flatpak/libportal/issues/136 +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1058245 +Signed-off-by: Simon McVittie +--- + tests/pyportaltest/__init__.py | 10 +++++++++- + 1 file changed, 9 insertions(+), 1 deletion(-) + +diff --git a/tests/pyportaltest/__init__.py b/tests/pyportaltest/__init__.py +index af053c2a..80f04a91 100644 +--- a/tests/pyportaltest/__init__.py ++++ b/tests/pyportaltest/__init__.py +@@ -83,6 +83,14 @@ def setUpClass(cls): + except AttributeError: + pytest.skip("Updated version of dbusmock required") + ++ cls.__have_session_bus = False ++ ++ @classmethod ++ def ensure_session_bus(cls): ++ if not cls.__have_session_bus: ++ cls.__have_session_bus = True ++ cls.start_session_bus() ++ + def setUp(self): + self.p_mock = None + self._mainloop = None +@@ -96,7 +104,7 @@ def setup_daemon(self, params=None, extra_templates: List[Tuple[str, Dict]] = [] + portal name as first value and the param dict to be passed to that + template as second value, e.g. ("ScreenCast", {...}). + """ +- self.start_session_bus() ++ self.ensure_session_bus() + self.p_mock, self.obj_portal = self.spawn_server_template( + template=f"pyportaltest/templates/{self.PORTAL_NAME.lower()}.py", + parameters=params, diff --git a/dev-libs/libportal/libportal-0.7.1-r1.ebuild b/dev-libs/libportal/libportal-0.7.1-r1.ebuild new file mode 100644 index 0000000..dd7b335 --- /dev/null +++ b/dev-libs/libportal/libportal-0.7.1-r1.ebuild @@ -0,0 +1,118 @@ +# Copyright 2022-2024 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +EAPI=8 + +PYTHON_COMPAT=( python3_{10..12} ) +inherit flag-o-matic meson python-any-r1 vala virtualx + +DESCRIPTION="Flatpak portal library" +HOMEPAGE="https://github.com/flatpak/libportal" +SRC_URI="https://github.com/flatpak/libportal/releases/download/${PV}/${P}.tar.xz" + +LICENSE="LGPL-3" +SLOT="0/1-1-1-1" # soname of libportal{,-gtk3,-gtk4,-qt5}.so +KEYWORDS="~alpha amd64 ~arm arm64 ~ia64 ~loong ~ppc ~ppc64 ~riscv ~sparc x86" +IUSE="gtk gtk-doc +introspection qt5 test +vala wayland X" +RESTRICT="!test? ( test )" +REQUIRED_USE=" + gtk-doc? ( introspection ) + vala? ( introspection ) +" + +RDEPEND=" + >=dev-libs/glib-2.58:2 + introspection? ( dev-libs/gobject-introspection:= ) + gtk? ( + >=x11-libs/gtk+-3.24.41-r1:3[X?,wayland?] + >=gui-libs/gtk-4.12.5-r2:4[X?,wayland?] + ) + qt5? ( + dev-qt/qtcore:= + dev-qt/qtgui:= + dev-qt/qtx11extras:= + dev-qt/qtwidgets:= + ) +" +DEPEND="${RDEPEND} + qt5? ( + test? ( dev-qt/qttest:= ) + ) +" +BDEPEND=" + dev-util/glib-utils + virtual/pkgconfig + gtk-doc? ( dev-util/gi-docgen ) + qt5? ( + test? ( dev-qt/linguist-tools ) + ) + test? ( + ${PYTHON_DEPS} + $(python_gen_any_dep ' + dev-python/pytest[${PYTHON_USEDEP}] + dev-python/dbus-python[${PYTHON_USEDEP}] + dev-python/python-dbusmock[${PYTHON_USEDEP}] + ') + ) + vala? ( $(vala_depend) ) +" + +PATCHES=( + # backport fix for tests incompatibility with dbusmock 0.30.0 + "${FILESDIR}"/6cd7c2ab82575b76f876ee2bd2d31f6cb77f022f.patch +) + +python_check_deps() { + python_has_version \ + "dev-python/pytest[${PYTHON_USEDEP}]" \ + "dev-python/dbus-python[${PYTHON_USEDEP}]" \ + "dev-python/python-dbusmock[${PYTHON_USEDEP}]" +} + +pkg_setup() { + if use test; then + python-any-r1_pkg_setup + fi +} + +src_prepare() { + default + vala_setup +} + +src_configure() { + # defang automagic dependencies + use wayland || append-cflags -DGENTOO_GTK_HIDE_WAYLAND + use X || append-cflags -DGENTOO_GTK_HIDE_X11 + + local emesonargs=( + $(meson_feature gtk backend-gtk3) + $(meson_feature gtk backend-gtk4) + $(meson_feature qt5 backend-qt5) + -Dportal-tests=false + $(meson_use introspection) + $(meson_use vala vapi) + $(meson_use gtk-doc docs) + $(meson_use test tests) + ) + meson_src_configure +} + +src_test() { + # Tests only exist for Qt5 + if use qt5; then + virtx meson_src_test + else + # run meson_src_test to notice if tests are added + meson_src_test + fi +} + +src_install() { + meson_src_install + + if use gtk-doc; then + mkdir -p "${ED}"/usr/share/gtk-doc/html/ || die + mv "${ED}"/usr/share/doc/${PN}-1 "${ED}"/usr/share/gtk-doc/html/ || die + fi +} diff --git a/dev-libs/libportal/libportal-0.7.1-r2.ebuild b/dev-libs/libportal/libportal-0.7.1-r2.ebuild new file mode 100644 index 0000000..f507b14 --- /dev/null +++ b/dev-libs/libportal/libportal-0.7.1-r2.ebuild @@ -0,0 +1,121 @@ +# Copyright 2022-2024 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +EAPI=8 + +PYTHON_COMPAT=( python3_{10..12} ) +inherit flag-o-matic meson python-any-r1 vala virtualx + +DESCRIPTION="Flatpak portal library" +HOMEPAGE="https://github.com/flatpak/libportal" +SRC_URI="https://github.com/flatpak/libportal/releases/download/${PV}/${P}.tar.xz" + +LICENSE="LGPL-3" +SLOT="0/1-1-1-1" # soname of libportal{,-gtk3,-gtk4,-qt5}.so +KEYWORDS="~alpha amd64 ~arm arm64 ~ia64 ~loong ~ppc ~ppc64 ~riscv ~sparc x86" +IUSE="gtk gtk-doc +introspection inputcapture qt5 test +vala wayland X" +RESTRICT="!test? ( test )" +REQUIRED_USE=" + gtk-doc? ( introspection ) + vala? ( introspection ) +" + +RDEPEND=" + >=dev-libs/glib-2.58:2 + introspection? ( dev-libs/gobject-introspection:= ) + gtk? ( + >=x11-libs/gtk+-3.24.41-r1:3[X?,wayland?] + >=gui-libs/gtk-4.12.5-r2:4[X?,wayland?] + ) + qt5? ( + dev-qt/qtcore:= + dev-qt/qtgui:= + dev-qt/qtx11extras:= + dev-qt/qtwidgets:= + ) +" +DEPEND="${RDEPEND} + qt5? ( + test? ( dev-qt/qttest:= ) + ) +" +BDEPEND=" + dev-util/glib-utils + virtual/pkgconfig + gtk-doc? ( dev-util/gi-docgen ) + qt5? ( + test? ( dev-qt/linguist-tools ) + ) + test? ( + ${PYTHON_DEPS} + $(python_gen_any_dep ' + dev-python/pytest[${PYTHON_USEDEP}] + dev-python/dbus-python[${PYTHON_USEDEP}] + dev-python/python-dbusmock[${PYTHON_USEDEP}] + ') + ) + vala? ( $(vala_depend) ) +" + +PATCHES=( + # backport fix for tests incompatibility with dbusmock 0.30.0 + "${FILESDIR}"/6cd7c2ab82575b76f876ee2bd2d31f6cb77f022f.patch +) + +python_check_deps() { + python_has_version \ + "dev-python/pytest[${PYTHON_USEDEP}]" \ + "dev-python/dbus-python[${PYTHON_USEDEP}]" \ + "dev-python/python-dbusmock[${PYTHON_USEDEP}]" +} + +pkg_setup() { + if use test; then + python-any-r1_pkg_setup + fi +} + +src_prepare() { + default + if use inputcapture; then + eapply "${FILESDIR}"/0001-Input-capture-support.patch + fi + vala_setup +} + +src_configure() { + # defang automagic dependencies + use wayland || append-cflags -DGENTOO_GTK_HIDE_WAYLAND + use X || append-cflags -DGENTOO_GTK_HIDE_X11 + + local emesonargs=( + $(meson_feature gtk backend-gtk3) + $(meson_feature gtk backend-gtk4) + $(meson_feature qt5 backend-qt5) + -Dportal-tests=false + $(meson_use introspection) + $(meson_use vala vapi) + $(meson_use gtk-doc docs) + $(meson_use test tests) + ) + meson_src_configure +} + +src_test() { + # Tests only exist for Qt5 + if use qt5; then + virtx meson_src_test + else + # run meson_src_test to notice if tests are added + meson_src_test + fi +} + +src_install() { + meson_src_install + + if use gtk-doc; then + mkdir -p "${ED}"/usr/share/gtk-doc/html/ || die + mv "${ED}"/usr/share/doc/${PN}-1 "${ED}"/usr/share/gtk-doc/html/ || die + fi +} diff --git a/dev-libs/libportal/libportal-0.7.1.ebuild b/dev-libs/libportal/libportal-0.7.1.ebuild new file mode 100644 index 0000000..5d36d44 --- /dev/null +++ b/dev-libs/libportal/libportal-0.7.1.ebuild @@ -0,0 +1,114 @@ +# Copyright 2022-2024 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +EAPI=8 + +PYTHON_COMPAT=( python3_{10..12} ) +inherit meson python-any-r1 vala virtualx + +DESCRIPTION="Flatpak portal library" +HOMEPAGE="https://github.com/flatpak/libportal" +SRC_URI="https://github.com/flatpak/libportal/releases/download/${PV}/${P}.tar.xz" + +LICENSE="LGPL-3" +SLOT="0/1-1-1-1" # soname of libportal{,-gtk3,-gtk4,-qt5}.so +KEYWORDS="~alpha amd64 ~arm arm64 ~ia64 ~loong ~ppc ~ppc64 ~riscv ~sparc x86" +IUSE="gtk gtk-doc +introspection qt5 test +vala" +RESTRICT="!test? ( test )" +REQUIRED_USE=" + gtk-doc? ( introspection ) + vala? ( introspection ) +" + +RDEPEND=" + >=dev-libs/glib-2.58:2 + introspection? ( dev-libs/gobject-introspection:= ) + gtk? ( + x11-libs/gtk+:3 + gui-libs/gtk:4 + ) + qt5? ( + dev-qt/qtcore:= + dev-qt/qtgui:= + dev-qt/qtx11extras:= + dev-qt/qtwidgets:= + ) +" +DEPEND="${RDEPEND} + qt5? ( + test? ( dev-qt/qttest:= ) + ) +" +BDEPEND=" + dev-util/glib-utils + virtual/pkgconfig + gtk-doc? ( dev-util/gi-docgen ) + qt5? ( + test? ( dev-qt/linguist-tools ) + ) + test? ( + ${PYTHON_DEPS} + $(python_gen_any_dep ' + dev-python/pytest[${PYTHON_USEDEP}] + dev-python/dbus-python[${PYTHON_USEDEP}] + dev-python/python-dbusmock[${PYTHON_USEDEP}] + ') + ) + vala? ( $(vala_depend) ) +" + +PATCHES=( + # backport fix for tests incompatibility with dbusmock 0.30.0 + "${FILESDIR}"/6cd7c2ab82575b76f876ee2bd2d31f6cb77f022f.patch +) + +python_check_deps() { + python_has_version \ + "dev-python/pytest[${PYTHON_USEDEP}]" \ + "dev-python/dbus-python[${PYTHON_USEDEP}]" \ + "dev-python/python-dbusmock[${PYTHON_USEDEP}]" +} + +pkg_setup() { + if use test; then + python-any-r1_pkg_setup + fi +} + +src_prepare() { + default + vala_setup +} + +src_configure() { + local emesonargs=( + $(meson_feature gtk backend-gtk3) + $(meson_feature gtk backend-gtk4) + $(meson_feature qt5 backend-qt5) + -Dportal-tests=false + $(meson_use introspection) + $(meson_use vala vapi) + $(meson_use gtk-doc docs) + $(meson_use test tests) + ) + meson_src_configure +} + +src_test() { + # Tests only exist for Qt5 + if use qt5; then + virtx meson_src_test + else + # run meson_src_test to notice if tests are added + meson_src_test + fi +} + +src_install() { + meson_src_install + + if use gtk-doc; then + mkdir -p "${ED}"/usr/share/gtk-doc/html/ || die + mv "${ED}"/usr/share/doc/${PN}-1 "${ED}"/usr/share/gtk-doc/html/ || die + fi +} diff --git a/dev-libs/libportal/metadata.xml b/dev-libs/libportal/metadata.xml new file mode 100644 index 0000000..5cda5e7 --- /dev/null +++ b/dev-libs/libportal/metadata.xml @@ -0,0 +1,11 @@ + + + + + gnome@gentoo.org + Gentoo GNOME Desktop + + + flatpak/libportal + +