projectmoon-overlay/dev-libs/libportal/files/0001-Input-capture-support....

3890 lines
129 KiB
Diff

From 4abbe42691dab79a17ff6a8ddfe5f17d2688a750 Mon Sep 17 00:00:00 2001
From: Peter Hutterer <peter.hutterer@who-t.net>
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 <http://www.gnu.org/licenses/>.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+#pragma once
+
+#include <libportal/portal-helpers.h>
+
+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 <http://www.gnu.org/licenses/>.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+#pragma once
+
+#include <libportal/portal-helpers.h>
+
+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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+#include "config.h"
+
+#include <gio/gunixfdlist.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+#pragma once
+
+#include <libportal/portal-helpers.h>
+#include <libportal/session.h>
+#include <libportal/inputcapture-zone.h>
+#include <libportal/inputcapture-pointerbarrier.h>
+#include <stdint.h>
+
+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 <libportal/email.h>
#include <libportal/filechooser.h>
#include <libportal/inhibit.h>
+#include <libportal/inputcapture.h>
#include <libportal/location.h>
#include <libportal/notification.h>
#include <libportal/openuri.h>
@@ -34,6 +35,7 @@
#include <libportal/print.h>
#include <libportal/remote.h>
#include <libportal/screenshot.h>
+#include <libportal/session.h>
#include <libportal/spawn.h>
#include <libportal/trash.h>
#include <libportal/types.h>
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 <libportal/types.h>
+#include <libportal/session.h>
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 <libportal/remote.h>
+#include <libportal/inputcapture.h>
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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-only
+ */
+
+#pragma once
+
+#include <libportal/types.h>
+
+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 @@
<property name="top-attach">19</property>
</packing>
</child>
+
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">1</property>
+ <property name="halign">end</property>
+ <property name="label">Input Capture</property>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">20</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="inputcapture_label">
+ <property name="visible">1</property>
+ <property name="halign">end</property>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">20</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">1</property>
+ <property name="hexpand">0</property>
+ <property name="orientation">horizontal</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkToggleButton" id="inputcapture_toggle">
+ <property name="visible">1</property>
+ <property name="hexpand">1</property>
+ <property name="label">Input Capture</property>
+ <signal name="clicked" handler="capture_input"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="enable">
+ <property name="visible">1</property>
+ <property name="tooltip-text">Enable</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">20</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">1</property>
+ <property name="hexpand">1</property>
+ <property name="label">Release</property>
+ <signal name="clicked" handler="capture_input_release"/>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">20</property>
+ </packing>
+ </child>
+
+
</object>
</child>
</template>
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