diff --git a/Makefile.am.inc b/Makefile.am.inc
index 7be4c4f89..e3d466e58 100644
--- a/Makefile.am.inc
+++ b/Makefile.am.inc
@@ -1,6 +1,7 @@
PORTAL_IFACE_FILES =\
$(top_srcdir)/data/org.freedesktop.portal.Account.xml \
$(top_srcdir)/data/org.freedesktop.portal.Background.xml \
+ $(top_srcdir)/data/org.freedesktop.portal.Clipboard.xml \
$(top_srcdir)/data/org.freedesktop.portal.Camera.xml \
$(top_srcdir)/data/org.freedesktop.portal.Device.xml \
$(top_srcdir)/data/org.freedesktop.portal.Documents.xml \
@@ -35,6 +36,7 @@ PORTAL_IMPL_IFACE_FILES =\
$(top_srcdir)/data/org.freedesktop.impl.portal.Account.xml \
$(top_srcdir)/data/org.freedesktop.impl.portal.AppChooser.xml \
$(top_srcdir)/data/org.freedesktop.impl.portal.Background.xml \
+ $(top_srcdir)/data/org.freedesktop.impl.portal.Clipboard.xml \
$(top_srcdir)/data/org.freedesktop.impl.portal.DynamicLauncher.xml \
$(top_srcdir)/data/org.freedesktop.impl.portal.Email.xml \
$(top_srcdir)/data/org.freedesktop.impl.portal.FileChooser.xml \
diff --git a/data/meson.build b/data/meson.build
index 489479f18..7762ebbbd 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -5,6 +5,7 @@ portal_sources = files(
'org.freedesktop.portal.Account.xml',
'org.freedesktop.portal.Background.xml',
'org.freedesktop.portal.Camera.xml',
+ 'org.freedesktop.portal.Clipboard.xml',
'org.freedesktop.portal.Device.xml',
'org.freedesktop.portal.Documents.xml',
'org.freedesktop.portal.DynamicLauncher.xml',
@@ -38,6 +39,7 @@ portal_impl_sources = files(
'org.freedesktop.impl.portal.Account.xml',
'org.freedesktop.impl.portal.AppChooser.xml',
'org.freedesktop.impl.portal.Background.xml',
+ 'org.freedesktop.impl.portal.Clipboard.xml',
'org.freedesktop.impl.portal.DynamicLauncher.xml',
'org.freedesktop.impl.portal.Email.xml',
'org.freedesktop.impl.portal.FileChooser.xml',
diff --git a/data/org.freedesktop.impl.portal.Clipboard.xml b/data/org.freedesktop.impl.portal.Clipboard.xml
new file mode 100644
index 000000000..89a240b8b
--- /dev/null
+++ b/data/org.freedesktop.impl.portal.Clipboard.xml
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/org.freedesktop.portal.Clipboard.xml b/data/org.freedesktop.portal.Clipboard.xml
new file mode 100644
index 000000000..18d48d180
--- /dev/null
+++ b/data/org.freedesktop.portal.Clipboard.xml
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/org.freedesktop.portal.RemoteDesktop.xml b/data/org.freedesktop.portal.RemoteDesktop.xml
index 9d18b2225..707eed59e 100644
--- a/data/org.freedesktop.portal.RemoteDesktop.xml
+++ b/data/org.freedesktop.portal.RemoteDesktop.xml
@@ -38,8 +38,8 @@
A remote desktop session may only be started and stopped with this interface,
but you can use the #org.freedesktop.portal.Session object created with this
- method together with certain methods on the #org.freedesktop.portal.ScreenCast
- interface. Specifically, you can call
+ method together with certain methods on the #org.freedesktop.portal.ScreenCast and
+ #org.freedesktop.portal.Clipboard interfaces. Specifically, you can call
org.freedesktop.portal.ScreenCast.SelectSources() to also get screen content,
and org.freedesktop.portal.ScreenCast.OpenPipewireRemote() to acquire a file
descriptor for a PipeWire remote. See #org.freedesktop.portal.ScreenCast for
diff --git a/doc/portal-docs.xml.in b/doc/portal-docs.xml.in
index 721e6f448..6c8d4566b 100644
--- a/doc/portal-docs.xml.in
+++ b/doc/portal-docs.xml.in
@@ -93,6 +93,7 @@
+
@@ -147,6 +148,7 @@
+
diff --git a/src/Makefile.am.inc b/src/Makefile.am.inc
index 86a8de0b9..1c8d82fa7 100644
--- a/src/Makefile.am.inc
+++ b/src/Makefile.am.inc
@@ -149,6 +149,8 @@ xdg_desktop_portal_SOURCES += \
src/pipewire.h \
src/camera.c \
src/camera.h \
+ src/clipboard.c \
+ src/clipboard.h \
$(NULL)
endif
diff --git a/src/clipboard.c b/src/clipboard.c
new file mode 100644
index 000000000..819d264db
--- /dev/null
+++ b/src/clipboard.c
@@ -0,0 +1,719 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * This program 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; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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 library. If not, see .
+ *
+ */
+
+#include
+#include
+
+#include "config.h"
+#include "remote-desktop.h"
+#include "request.h"
+#include "session.h"
+#include "xdp-dbus.h"
+#include "xdp-impl-dbus.h"
+#include "xdp-utils.h"
+
+typedef struct _Clipboard Clipboard;
+typedef struct _ClipboardClass ClipboardClass;
+
+struct _Clipboard {
+ XdpDbusClipboardSkeleton parent_instance;
+};
+
+struct _ClipboardClass {
+ XdpDbusClipboardSkeletonClass parent_class;
+};
+
+static XdpDbusImplClipboard *impl;
+static Clipboard *clipboard;
+
+GType clipboard_get_type(void) G_GNUC_CONST;
+static void clipboard_iface_init(XdpDbusClipboardIface *iface);
+
+static GQuark quark_request_session;
+
+G_DEFINE_TYPE_WITH_CODE(Clipboard, clipboard, XDP_DBUS_TYPE_CLIPBOARD_SKELETON,
+ G_IMPLEMENT_INTERFACE(XDP_DBUS_TYPE_CLIPBOARD,
+ clipboard_iface_init))
+
+static XdpOptionKey clipboard_set_selection_options[] = {};
+
+static XdpOptionKey clipboard_enable_options[] = {};
+
+static void selection_write_done_done(GObject *source_object, GAsyncResult *res,
+ gpointer data) {
+ g_autoptr(Request) request = data;
+ Session *session;
+ guint response = 2;
+ gboolean should_close_session;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) results = NULL;
+
+ REQUEST_AUTOLOCK(request)
+
+ session = g_object_get_qdata(G_OBJECT(request), quark_request_session);
+ SESSION_AUTOLOCK_UNREF(g_object_ref(session));
+ g_object_set_qdata(G_OBJECT(request), quark_request_session, NULL);
+
+ if (!xdp_dbus_impl_clipboard_call_selection_write_done_finish(
+ impl, &response, &results, res, &error)) {
+ g_dbus_error_strip_remote_error(error);
+ g_warning("A backend call failed: %s:", error->message);
+ }
+
+ should_close_session = !request->exported || response != 0;
+
+ if (request->exported) {
+ if (!results) {
+ GVariantBuilder results_builder;
+
+ g_variant_builder_init(&results_builder, G_VARIANT_TYPE_VARDICT);
+ results = g_variant_ref_sink(g_variant_builder_end(&results_builder));
+ }
+
+ xdp_dbus_request_emit_response(XDP_DBUS_REQUEST(request), response,
+ results);
+ request_unexport(request);
+ }
+
+ if (should_close_session) {
+ session_close(session, TRUE);
+ }
+}
+
+static void selection_read_done(GObject *source_object, GAsyncResult *res,
+ gpointer data) {
+ g_autoptr(Request) request = data;
+ Session *session;
+ guint response = 2;
+ gboolean should_close_session;
+ GVariantBuilder results_builder;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) results = NULL;
+ g_autoptr(GUnixFDList) out_fd_list = NULL;
+
+ REQUEST_AUTOLOCK(request);
+ session = g_object_get_qdata(G_OBJECT(request), quark_request_session);
+
+ g_variant_builder_init(&results_builder, G_VARIANT_TYPE_VARDICT);
+ SESSION_AUTOLOCK_UNREF(g_object_ref(session));
+ g_object_set_qdata(G_OBJECT(request), quark_request_session, NULL);
+
+ if (!xdp_dbus_impl_clipboard_call_selection_read_finish(
+ impl, &response, &results, &out_fd_list, res, &error)) {
+ g_dbus_error_strip_remote_error(error);
+ g_warning("A backend call failed: %s:", error->message);
+ }
+
+ should_close_session = !request->exported || response != 0;
+
+ if (results) {
+ int fd;
+ int fd_id;
+ g_autofree char *path = NULL;
+
+ if (g_variant_lookup(results, "fd", "h", &fd_id)) {
+ fd = g_unix_fd_list_get(out_fd_list, fd_id, &error);
+
+ path = xdp_app_info_get_path_for_fd(request->app_info, fd, 0, NULL, NULL,
+ &error); // TODO validate?
+
+ // close(fd); // should I close this if I can't send the fd list?
+
+ if (!path) {
+ g_warning("invalid file descriptor");
+ } else {
+ g_variant_builder_add(&results_builder, "{sv}", "fd",
+ g_variant_new_int32(fd));
+ // g_variant_new_handle(fd) // how to return fd list??
+ }
+ }
+ }
+
+ if (request->exported) {
+ xdp_dbus_request_emit_response(XDP_DBUS_REQUEST(request), response,
+ g_variant_builder_end(&results_builder));
+ request_unexport(request);
+ } else {
+ g_variant_builder_clear(&results_builder);
+ }
+
+ if (should_close_session) {
+ session_close(session, TRUE);
+ }
+}
+
+static void set_selection_done(GObject *source_object, GAsyncResult *res,
+ gpointer data) {
+ g_autoptr(Request) request = data;
+ Session *session;
+ guint response = 2;
+ gboolean should_close_session;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) results = NULL;
+
+ REQUEST_AUTOLOCK(request);
+
+ session = g_object_get_qdata(G_OBJECT(request), quark_request_session);
+ SESSION_AUTOLOCK_UNREF(g_object_ref(session));
+ g_object_set_qdata(G_OBJECT(request), quark_request_session, NULL);
+
+ if (!xdp_dbus_impl_clipboard_call_set_selection_finish(
+ impl, &response, &results, res, &error)) {
+ g_dbus_error_strip_remote_error(error);
+ g_warning("A backend call failed: %s:", error->message);
+ }
+
+ should_close_session = !request->exported || response != 0;
+
+ if (request->exported) {
+ if (!results) {
+ GVariantBuilder results_builder;
+
+ g_variant_builder_init(&results_builder, G_VARIANT_TYPE_VARDICT);
+ results = g_variant_ref_sink(g_variant_builder_end(&results_builder));
+ }
+
+ xdp_dbus_request_emit_response(XDP_DBUS_REQUEST(request), response,
+ results);
+ request_unexport(request);
+
+ if (should_close_session) {
+ session_close(session, TRUE);
+ }
+ }
+}
+
+static void selection_write_done(GObject *source_object, GAsyncResult *res,
+ gpointer data) {
+ g_autoptr(Request) request = data;
+ Session *session;
+ guint response = 2;
+ gboolean should_close_session;
+ GVariantBuilder results_builder;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) results = NULL;
+ g_autoptr(GUnixFDList) out_fd_list = NULL;
+
+ REQUEST_AUTOLOCK(request);
+ session = g_object_get_qdata(G_OBJECT(request), quark_request_session);
+
+ g_variant_builder_init(&results_builder, G_VARIANT_TYPE_VARDICT);
+ SESSION_AUTOLOCK_UNREF(g_object_ref(session));
+ g_object_set_qdata(G_OBJECT(request), quark_request_session, NULL);
+
+ if (!xdp_dbus_impl_clipboard_call_selection_write_finish(
+ impl, &response, &results, &out_fd_list, res, &error)) {
+ g_dbus_error_strip_remote_error(error);
+ g_warning("A backend call failed: %s:", error->message);
+ }
+
+ should_close_session = !request->exported || response != 0;
+
+ if (results) {
+ int fd;
+ int fd_id;
+ g_autofree char *path = NULL;
+
+ if (g_variant_lookup(results, "fd", "h", &fd_id)) {
+ fd = g_unix_fd_list_get(out_fd_list, fd_id, &error);
+
+ path = xdp_app_info_get_path_for_fd(request->app_info, fd, 0, NULL, NULL,
+ &error); // TODO validate?
+ // close(fd); // should I close this if I can't send the fd list?
+
+ if (!path) {
+ g_warning("invalid file descriptor");
+ } else {
+ g_variant_builder_add(&results_builder, "{sv}", "fd",
+ g_variant_new_int32(fd));
+ // g_variant_new_handle(fd) // how to return fd list??
+ }
+ }
+ }
+
+ if (request->exported) {
+ xdp_dbus_request_emit_response(XDP_DBUS_REQUEST(request), response,
+ g_variant_builder_end(&results_builder));
+ request_unexport(request);
+ } else {
+ g_variant_builder_clear(&results_builder);
+ }
+
+ if (should_close_session) {
+ session_close(session, TRUE);
+ }
+}
+
+static void enable_clipboard_done(GObject *source_object, GAsyncResult *res,
+ gpointer data) {
+ g_autoptr(Request) request = data;
+ Session *session;
+ guint response = 2;
+ gboolean should_close_session;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) results = NULL;
+
+ REQUEST_AUTOLOCK(request)
+
+ session = g_object_get_qdata(G_OBJECT(request), quark_request_session);
+ SESSION_AUTOLOCK_UNREF(g_object_ref(session));
+ g_object_set_qdata(G_OBJECT(request), quark_request_session, NULL);
+
+ if (!xdp_dbus_impl_clipboard_call_enable_clipboard_finish(
+ impl, &response, &results, res, &error)) {
+ g_dbus_error_strip_remote_error(error);
+ g_warning("A backend call failed: %s:", error->message);
+ }
+
+ should_close_session = !request->exported || response != 0;
+
+ if (request->exported) {
+ if (!results) {
+ GVariantBuilder results_builder;
+
+ g_variant_builder_init(&results_builder, G_VARIANT_TYPE_VARDICT);
+ results = g_variant_ref_sink(g_variant_builder_end(&results_builder));
+ }
+
+ xdp_dbus_request_emit_response(XDP_DBUS_REQUEST(request), response,
+ results);
+ request_unexport(request);
+ }
+
+ if (should_close_session) {
+ session_close(session, TRUE);
+ } else if (!session->closed) {
+ RemoteDesktopSession *remote_desktop_session =
+ (RemoteDesktopSession *)session;
+ remote_desktop_session_enabled_clipboard(remote_desktop_session);
+ }
+}
+
+static gboolean handle_enable_clipboard(XdpDbusClipboard *object,
+ GDBusMethodInvocation *invocation,
+ const gchar *arg_session_handle,
+ GVariant *arg_options) {
+ Request *request = request_from_invocation(invocation);
+ Session *session;
+ RemoteDesktopSession *remote_desktop_session;
+ GVariantBuilder options_builder;
+ GVariant *options;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(XdpDbusImplRequest) impl_request = NULL;
+
+ session = acquire_session(arg_session_handle, request);
+ if (!session) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Invalid session");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ SESSION_AUTOLOCK_UNREF(session);
+
+ if (!is_remote_desktop_session(session)) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Invalid session type");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+ remote_desktop_session = (RemoteDesktopSession *)session;
+
+ if (!remote_desktop_session_can_enable_clipboard(remote_desktop_session)) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_FAILED, "Invalid state");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ impl_request = xdp_dbus_impl_request_proxy_new_sync(
+ g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)), G_DBUS_PROXY_FLAGS_NONE,
+ g_dbus_proxy_get_name(G_DBUS_PROXY(impl)), request->id, NULL, &error);
+ if (!impl_request) {
+ g_dbus_method_invocation_return_gerror(invocation, error);
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ request_set_impl_request(request, impl_request);
+ request_export(request, g_dbus_method_invocation_get_connection(invocation));
+
+ g_object_set_qdata_full(G_OBJECT(request), quark_request_session,
+ g_object_ref(session), g_object_unref);
+
+ g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT);
+ if (!xdp_filter_options(arg_options, &options_builder,
+ clipboard_enable_options,
+ G_N_ELEMENTS(clipboard_enable_options), &error)) {
+ g_dbus_method_invocation_return_gerror(invocation, error);
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+ options = g_variant_builder_end(&options_builder);
+
+ remote_desktop_session_enabling_clipboard(remote_desktop_session);
+
+ xdp_dbus_impl_clipboard_call_enable_clipboard(
+ impl, request->id, arg_session_handle,
+ xdp_app_info_get_id(request->app_info), options, NULL,
+ enable_clipboard_done, g_object_ref(request));
+
+ xdp_dbus_clipboard_complete_enable_clipboard(object, invocation, request->id);
+
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+static gboolean handle_selection_read(XdpDbusClipboard *object,
+ GDBusMethodInvocation *invocation,
+ GUnixFDList *in_fd_list,
+ const gchar *arg_session_handle,
+ const gchar *arg_mime_type,
+ GVariant *arg_options) {
+ Request *request = request_from_invocation(invocation);
+ Session *session;
+ GVariantBuilder options_builder;
+ GVariant *options;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(XdpDbusImplRequest) impl_request = NULL;
+
+ session = acquire_session(arg_session_handle, request);
+ if (!session) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Invalid session");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ SESSION_AUTOLOCK_UNREF(session);
+
+ if (!is_remote_desktop_session(session)) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Invalid session type");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ } else if (!remote_desktop_session_is_clipboard_enabled(
+ (RemoteDesktopSession *)session)) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Clipboard not enabled");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ impl_request = xdp_dbus_impl_request_proxy_new_sync(
+ g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)), G_DBUS_PROXY_FLAGS_NONE,
+ g_dbus_proxy_get_name(G_DBUS_PROXY(impl)), request->id, NULL, &error);
+ if (!impl_request) {
+ g_dbus_method_invocation_return_gerror(invocation, error);
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ request_set_impl_request(request, impl_request);
+ request_export(request, g_dbus_method_invocation_get_connection(invocation));
+
+ g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT);
+ options = g_variant_builder_end(&options_builder);
+
+ g_object_set_qdata_full(G_OBJECT(request), quark_request_session,
+ g_object_ref(session), g_object_unref);
+
+ xdp_dbus_impl_clipboard_call_selection_read(
+ impl, request->id, arg_session_handle,
+ xdp_app_info_get_id(request->app_info), arg_mime_type, options, NULL,
+ NULL, selection_read_done, g_object_ref(request));
+
+ xdp_dbus_clipboard_complete_selection_read(object, invocation, NULL,
+ request->id);
+
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+static gboolean handle_selection_write(XdpDbusClipboard *object,
+ GDBusMethodInvocation *invocation,
+ GUnixFDList *in_fd_list,
+ const gchar *arg_session_handle,
+ guint arg_serial,
+ GVariant *arg_options) {
+ Request *request = request_from_invocation(invocation);
+ Session *session;
+ GVariantBuilder options_builder;
+ GVariant *options;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(XdpDbusImplRequest) impl_request = NULL;
+
+ session = acquire_session(arg_session_handle, request);
+ if (!session) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Invalid session");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ SESSION_AUTOLOCK_UNREF(session);
+
+ if (!is_remote_desktop_session(session)) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Invalid session type");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ } else if (!remote_desktop_session_is_clipboard_enabled(
+ (RemoteDesktopSession *)session)) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Clipboard not enabled");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ impl_request = xdp_dbus_impl_request_proxy_new_sync(
+ g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)), G_DBUS_PROXY_FLAGS_NONE,
+ g_dbus_proxy_get_name(G_DBUS_PROXY(impl)), request->id, NULL, &error);
+ if (!impl_request) {
+ g_dbus_method_invocation_return_gerror(invocation, error);
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ request_set_impl_request(request, impl_request);
+ request_export(request, g_dbus_method_invocation_get_connection(invocation));
+
+ g_object_set_qdata_full(G_OBJECT(request), quark_request_session,
+ g_object_ref(session), g_object_unref);
+
+ g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT);
+ options = g_variant_builder_end(&options_builder);
+
+ xdp_dbus_impl_clipboard_call_selection_write(
+ impl, request->id, arg_session_handle,
+ xdp_app_info_get_id(request->app_info), arg_serial, options, NULL, NULL,
+ selection_write_done, g_object_ref(request));
+
+ xdp_dbus_clipboard_complete_selection_write(object, invocation, NULL,
+ request->id);
+
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+static gboolean handle_set_selection(XdpDbusClipboard *object,
+ GDBusMethodInvocation *invocation,
+ const gchar *arg_session_handle,
+ GVariant *arg_options) {
+ Request *request = request_from_invocation(invocation);
+ Session *session;
+ GVariantBuilder options_builder;
+ GVariant *options;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(XdpDbusImplRequest) impl_request = NULL;
+
+ session = acquire_session(arg_session_handle, request);
+ if (!session) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Invalid session");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ SESSION_AUTOLOCK_UNREF(session);
+
+ if (!is_remote_desktop_session(session)) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Invalid session type");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ } else if (!remote_desktop_session_is_clipboard_enabled(
+ (RemoteDesktopSession *)session)) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Clipboard not enabled");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ impl_request = xdp_dbus_impl_request_proxy_new_sync(
+ g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)), G_DBUS_PROXY_FLAGS_NONE,
+ g_dbus_proxy_get_name(G_DBUS_PROXY(impl)), request->id, NULL, &error);
+ if (!impl_request) {
+ g_dbus_method_invocation_return_gerror(invocation, error);
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ request_set_impl_request(request, impl_request);
+ request_export(request, g_dbus_method_invocation_get_connection(invocation));
+
+ g_object_set_qdata_full(G_OBJECT(request), quark_request_session,
+ g_object_ref(session), g_object_unref);
+
+ g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT);
+ if (!xdp_filter_options(
+ arg_options, &options_builder, clipboard_set_selection_options,
+ G_N_ELEMENTS(clipboard_set_selection_options), &error)) {
+ g_dbus_method_invocation_return_gerror(invocation, error);
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+ options = g_variant_builder_end(&options_builder);
+
+ xdp_dbus_impl_clipboard_call_set_selection(
+ impl, request->id, arg_session_handle,
+ xdp_app_info_get_id(request->app_info), options, NULL, set_selection_done,
+ g_object_ref(request));
+
+ xdp_dbus_clipboard_complete_set_selection(object, invocation, request->id);
+
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+static gboolean handle_selection_write_done(XdpDbusClipboard *object,
+ GDBusMethodInvocation *invocation,
+ const gchar *arg_session_handle,
+ guint arg_serial,
+ gboolean arg_success,
+ GVariant *arg_options) {
+ Request *request = request_from_invocation(invocation);
+ Session *session;
+ GVariantBuilder options_builder;
+ GVariant *options;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(XdpDbusImplRequest) impl_request = NULL;
+
+ session = acquire_session(arg_session_handle, request);
+ if (!session) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Invalid session");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ SESSION_AUTOLOCK_UNREF(session);
+
+ if (!is_remote_desktop_session(session)) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Invalid session type");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ } else if (!remote_desktop_session_is_clipboard_enabled(
+ (RemoteDesktopSession *)session)) {
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_ACCESS_DENIED,
+ "Clipboard not enabled");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ impl_request = xdp_dbus_impl_request_proxy_new_sync(
+ g_dbus_proxy_get_connection(G_DBUS_PROXY(impl)), G_DBUS_PROXY_FLAGS_NONE,
+ g_dbus_proxy_get_name(G_DBUS_PROXY(impl)), request->id, NULL, &error);
+ if (!impl_request) {
+ g_dbus_method_invocation_return_gerror(invocation, error);
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+ }
+
+ request_set_impl_request(request, impl_request);
+ request_export(request, g_dbus_method_invocation_get_connection(invocation));
+
+ g_object_set_qdata_full(G_OBJECT(request), quark_request_session,
+ g_object_ref(session), g_object_unref);
+
+ g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT);
+ options = g_variant_builder_end(&options_builder);
+
+ xdp_dbus_impl_clipboard_call_selection_write_done(
+ impl, request->id, arg_session_handle,
+ xdp_app_info_get_id(request->app_info), arg_serial, arg_success, options,
+ NULL, selection_write_done_done, g_object_ref(request));
+
+ xdp_dbus_clipboard_complete_selection_write_done(object, invocation,
+ request->id);
+
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+static void clipboard_iface_init(XdpDbusClipboardIface *iface) {
+ iface->handle_enable_clipboard = handle_enable_clipboard;
+
+ iface->handle_selection_read = handle_selection_read;
+ iface->handle_selection_write = handle_selection_write;
+ iface->handle_set_selection = handle_set_selection;
+ iface->handle_selection_write_done = handle_selection_write_done;
+}
+
+static void clipboard_init(Clipboard *clipboard) {
+ xdp_dbus_clipboard_set_version(XDP_DBUS_CLIPBOARD(clipboard), 1);
+}
+
+static void clipboard_class_init(ClipboardClass *klass) {
+ quark_request_session =
+ g_quark_from_static_string("-xdp-request-clipboard-session");
+}
+
+static void selection_transfer_cb(XdpDbusImplClipboard *impl,
+ const char *arg_session_handle,
+ const gchar *arg_mime_type, guint arg_serial,
+ gpointer data) {
+ GDBusConnection *connection = g_dbus_proxy_get_connection(G_DBUS_PROXY(impl));
+ g_autoptr(Session) session = lookup_session(arg_session_handle);
+
+ RemoteDesktopSession *remote_desktop_session =
+ (RemoteDesktopSession *)session;
+
+ if (remote_desktop_session &&
+ remote_desktop_session_is_clipboard_enabled(remote_desktop_session) &&
+ !session->closed) {
+ g_dbus_connection_emit_signal(
+ connection, session->sender, "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.Clipboard", "SelectionTransfer",
+ g_variant_new("(o@a{sv})", arg_session_handle, arg_mime_type,
+ arg_serial),
+ NULL);
+ }
+}
+
+static void selection_owner_changed_cb(XdpDbusImplClipboard *impl,
+ const char *arg_session_handle,
+ GVariant *arg_options, gpointer data) {
+ GDBusConnection *connection = g_dbus_proxy_get_connection(G_DBUS_PROXY(impl));
+ g_autoptr(Session) session = lookup_session(arg_session_handle);
+
+ RemoteDesktopSession *remote_desktop_session =
+ (RemoteDesktopSession *)session;
+
+ if (remote_desktop_session &&
+ remote_desktop_session_is_clipboard_enabled(remote_desktop_session) &&
+ !session->closed) {
+ g_dbus_connection_emit_signal(
+ connection, session->sender, "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.Clipboard", "SelectionTransfer",
+ g_variant_new("(o@a{sv})", arg_session_handle, arg_options), NULL);
+ }
+}
+
+GDBusInterfaceSkeleton *clipboard_create(GDBusConnection *connection,
+ const char *dbus_name) {
+ g_autoptr(GError) error = NULL;
+
+ impl = xdp_dbus_impl_clipboard_proxy_new_sync(
+ connection, G_DBUS_PROXY_FLAGS_NONE, dbus_name,
+ DESKTOP_PORTAL_OBJECT_PATH, NULL, &error);
+ if (impl == NULL) {
+ g_warning("Failed to create clipboard: %s", error->message);
+ return NULL;
+ }
+
+ g_dbus_proxy_set_default_timeout(G_DBUS_PROXY(impl), G_MAXINT);
+
+ clipboard = g_object_new(clipboard_get_type(), NULL);
+
+ g_signal_connect(impl, "selection-transfer",
+ G_CALLBACK(selection_transfer_cb), clipboard);
+
+ g_signal_connect(impl, "selection-owner-changed",
+ G_CALLBACK(selection_owner_changed_cb), clipboard);
+
+ return G_DBUS_INTERFACE_SKELETON(clipboard);
+}
diff --git a/src/clipboard.h b/src/clipboard.h
new file mode 100644
index 000000000..2e6e38fea
--- /dev/null
+++ b/src/clipboard.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * This program 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; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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 library. If not, see .
+ *
+ */
+
+#pragma once
+
+#include
+
+#include "session.h"
+
+GDBusInterfaceSkeleton *clipboard_create(GDBusConnection *connection,
+ const char *dbus_name);
diff --git a/src/meson.build b/src/meson.build
index 24227fe4f..04217c436 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -84,6 +84,7 @@ if have_pipewire
'remote-desktop.c',
'pipewire.c',
'camera.c',
+ 'clipboard.c',
)
endif
diff --git a/src/remote-desktop.c b/src/remote-desktop.c
index f450d6965..55ca2a002 100644
--- a/src/remote-desktop.c
+++ b/src/remote-desktop.c
@@ -56,13 +56,14 @@ G_DEFINE_TYPE_WITH_CODE (RemoteDesktop, remote_desktop,
G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_REMOTE_DESKTOP,
remote_desktop_iface_init))
-typedef enum _RemoteDesktopSessionState
-{
+typedef enum _RemoteDesktopSessionState {
REMOTE_DESKTOP_SESSION_STATE_INIT,
REMOTE_DESKTOP_SESSION_STATE_SELECTING_DEVICES,
REMOTE_DESKTOP_SESSION_STATE_DEVICES_SELECTED,
REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES,
REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED,
+ REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD,
+ REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD,
REMOTE_DESKTOP_SESSION_STATE_STARTING,
REMOTE_DESKTOP_SESSION_STATE_STARTED,
REMOTE_DESKTOP_SESSION_STATE_CLOSED
@@ -85,6 +86,8 @@ typedef struct _RemoteDesktopSession
DeviceType shared_devices;
GList *streams;
+
+ gboolean clipboard_enabled;
} RemoteDesktopSession;
typedef struct _RemoteDesktopSessionClass
@@ -106,14 +109,15 @@ is_remote_desktop_session (Session *session)
gboolean
remote_desktop_session_can_select_sources (RemoteDesktopSession *session)
{
-
switch (session->state)
{
case REMOTE_DESKTOP_SESSION_STATE_INIT:
case REMOTE_DESKTOP_SESSION_STATE_SELECTING_DEVICES:
case REMOTE_DESKTOP_SESSION_STATE_DEVICES_SELECTED:
+ case REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD:
return TRUE;
case REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES:
+ case REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD:
case REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED:
case REMOTE_DESKTOP_SESSION_STATE_STARTING:
case REMOTE_DESKTOP_SESSION_STATE_STARTED:
@@ -124,6 +128,26 @@ remote_desktop_session_can_select_sources (RemoteDesktopSession *session)
g_assert_not_reached ();
}
+gboolean remote_desktop_session_can_enable_clipboard(
+ RemoteDesktopSession *session) {
+ switch (session->state) {
+ case REMOTE_DESKTOP_SESSION_STATE_INIT:
+ case REMOTE_DESKTOP_SESSION_STATE_SELECTING_DEVICES:
+ case REMOTE_DESKTOP_SESSION_STATE_DEVICES_SELECTED:
+ case REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED:
+ return TRUE;
+ case REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES:
+ case REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD:
+ case REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD:
+ case REMOTE_DESKTOP_SESSION_STATE_STARTING:
+ case REMOTE_DESKTOP_SESSION_STATE_STARTED:
+ case REMOTE_DESKTOP_SESSION_STATE_CLOSED:
+ return FALSE;
+ }
+
+ g_assert_not_reached();
+}
+
GList *
remote_desktop_session_get_streams (RemoteDesktopSession *session)
{
@@ -142,6 +166,20 @@ remote_desktop_session_sources_selected (RemoteDesktopSession *session)
session->state = REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED;
}
+gboolean remote_desktop_session_is_clipboard_enabled(
+ RemoteDesktopSession *session) {
+ return session->clipboard_enabled;
+}
+
+void remote_desktop_session_enabling_clipboard(RemoteDesktopSession *session) {
+ session->state = REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD;
+}
+
+void remote_desktop_session_enabled_clipboard(RemoteDesktopSession *session) {
+ session->clipboard_enabled = true;
+ session->state = REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD;
+}
+
static RemoteDesktopSession *
remote_desktop_session_new (GVariant *options,
Request *request,
@@ -415,7 +453,9 @@ handle_select_devices (XdpDbusRemoteDesktop *object,
case REMOTE_DESKTOP_SESSION_STATE_INIT:
case REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES:
case REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED:
- break;
+ case REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD:
+ case REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD:
+ break;
case REMOTE_DESKTOP_SESSION_STATE_SELECTING_DEVICES:
case REMOTE_DESKTOP_SESSION_STATE_DEVICES_SELECTED:
g_dbus_method_invocation_return_error (invocation,
@@ -610,7 +650,8 @@ handle_start (XdpDbusRemoteDesktop *object,
case REMOTE_DESKTOP_SESSION_STATE_INIT:
case REMOTE_DESKTOP_SESSION_STATE_DEVICES_SELECTED:
case REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED:
- break;
+ case REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD:
+ break;
case REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES:
g_dbus_method_invocation_return_error (invocation,
G_DBUS_ERROR,
@@ -623,6 +664,11 @@ handle_start (XdpDbusRemoteDesktop *object,
G_DBUS_ERROR_FAILED,
"Devices not selected");
return G_DBUS_METHOD_INVOCATION_HANDLED;
+ case REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD:
+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
+ G_DBUS_ERROR_FAILED,
+ "Clipboard not enabled");
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
case REMOTE_DESKTOP_SESSION_STATE_STARTING:
case REMOTE_DESKTOP_SESSION_STATE_STARTED:
g_dbus_method_invocation_return_error (invocation,
@@ -695,6 +741,8 @@ check_notify (Session *session,
case REMOTE_DESKTOP_SESSION_STATE_SELECTING_DEVICES:
case REMOTE_DESKTOP_SESSION_STATE_SOURCES_SELECTED:
case REMOTE_DESKTOP_SESSION_STATE_SELECTING_SOURCES:
+ case REMOTE_DESKTOP_SESSION_STATE_ENABLING_CLIPBOARD:
+ case REMOTE_DESKTOP_SESSION_STATE_ENABLED_CLIPBOARD:
case REMOTE_DESKTOP_SESSION_STATE_STARTING:
case REMOTE_DESKTOP_SESSION_STATE_CLOSED:
return FALSE;
diff --git a/src/remote-desktop.h b/src/remote-desktop.h
index 4b75e9b47..aba7ca3e2 100644
--- a/src/remote-desktop.h
+++ b/src/remote-desktop.h
@@ -31,9 +31,19 @@ GList * remote_desktop_session_get_streams (RemoteDesktopSession *session);
gboolean remote_desktop_session_can_select_sources (RemoteDesktopSession *session);
+gboolean remote_desktop_session_can_enable_clipboard(
+ RemoteDesktopSession *session);
+
+gboolean remote_desktop_session_is_clipboard_enabled(
+ RemoteDesktopSession *session);
+
void remote_desktop_session_selecting_sources (RemoteDesktopSession *session);
void remote_desktop_session_sources_selected (RemoteDesktopSession *session);
+void remote_desktop_session_enabling_clipboard(RemoteDesktopSession *session);
+
+void remote_desktop_session_enabled_clipboard(RemoteDesktopSession *session);
+
GDBusInterfaceSkeleton * remote_desktop_create (GDBusConnection *connection,
const char *dbus_name);
diff --git a/src/request.c b/src/request.c
index 5bd5016de..9af9fbf3c 100644
--- a/src/request.c
+++ b/src/request.c
@@ -268,8 +268,22 @@ get_token (GDBusMethodInvocation *invocation)
g_warning ("Support for %s::%s missing in %s",
interface, method, G_STRLOC);
}
- }
- else if (strcmp (interface, "org.freedesktop.portal.Location") == 0)
+ } else if (strcmp(interface, "org.freedesktop.portal.Clipboard") == 0) {
+ if (strcmp(method, "EnableClipboard") == 0) {
+ options = g_variant_get_child_value(parameters, 1);
+ } else if (strcmp(method, "SetSelection") == 0) {
+ options = g_variant_get_child_value(parameters, 1);
+ } else if (strcmp(method, "SelectionWrite") == 0) {
+ options = g_variant_get_child_value(parameters, 2);
+ } else if (strcmp(method, "SelectionWriteDone") == 0) {
+ options = g_variant_get_child_value(parameters, 3);
+ } else if (strcmp(method, "SelectionRead") == 0) {
+ options = g_variant_get_child_value(parameters, 2);
+ } else {
+ g_warning("Support for %s::%s missing in %s", interface, method,
+ G_STRLOC);
+ }
+ } else if (strcmp (interface, "org.freedesktop.portal.Location") == 0)
{
if (strcmp (method, "CreateSession") == 0 )
{
diff --git a/src/xdg-desktop-portal.c b/src/xdg-desktop-portal.c
index ad4d97075..43ee80736 100644
--- a/src/xdg-desktop-portal.c
+++ b/src/xdg-desktop-portal.c
@@ -60,6 +60,7 @@
#include "wallpaper.h"
#include "realtime.h"
#include "dynamic-launcher.h"
+#include "clipboard.h"
static GMainLoop *loop = NULL;
@@ -276,6 +277,12 @@ on_bus_acquired (GDBusConnection *connection,
export_portal_implementation (connection,
notification_create (connection, implementation->dbus_name));
+ implementation =
+ find_portal_implementation("org.freedesktop.impl.portal.Clipboard");
+ if (implementation != NULL)
+ export_portal_implementation(
+ connection, clipboard_create(connection, implementation->dbus_name));
+
implementation = find_portal_implementation ("org.freedesktop.impl.portal.Inhibit");
if (implementation != NULL)
export_portal_implementation (connection,
diff --git a/tests/__init__.py b/tests/__init__.py
index c64302e84..2e5ea02ab 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -294,6 +294,16 @@ def start_impl_portal(self, params=None, portal=None):
self.start_dbus_monitor()
+ def add_template(self, portal, params={}):
+ """
+ Add an additional template to the portal object
+ """
+ self.obj_portal.AddTemplate(
+ f"tests/templates/{portal.lower()}.py",
+ params,
+ dbus_interface=dbusmock.MOCK_IFACE,
+ )
+
def start_xdp(self):
"""
Start the xdg-desktop-portal process
diff --git a/tests/portals/test.portal b/tests/portals/test.portal
index e43f90e3d..29553d357 100644
--- a/tests/portals/test.portal
+++ b/tests/portals/test.portal
@@ -1,4 +1,4 @@
[portal]
DBusName=org.freedesktop.impl.portal.Test
-Interfaces=org.freedesktop.impl.portal.Account;org.freedesktop.impl.portal.Email;org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Lockdown;org.freedesktop.impl.portal.Print;org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Inhibit;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Notification;org.freedesktop.impl.portal.Settings;
+Interfaces=org.freedesktop.impl.portal.RemoteDesktop;org.freedesktop.impl.portal.Clipboard;org.freedesktop.impl.portal.Account;org.freedesktop.impl.portal.Email;org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Lockdown;org.freedesktop.impl.portal.Print;org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Inhibit;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Notification;org.freedesktop.impl.portal.Settings;
UseIn=test
diff --git a/tests/templates/clipboard.py b/tests/templates/clipboard.py
new file mode 100644
index 000000000..48535ab3a
--- /dev/null
+++ b/tests/templates/clipboard.py
@@ -0,0 +1,239 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is formatted with Python Black
+
+from tests.templates import Response, init_template_logger, ImplRequest
+import dbus.service
+import dbus
+import os, sys
+from gi.repository import GLib
+
+BUS_NAME = "org.freedesktop.impl.portal.Test"
+MAIN_OBJ = "/org/freedesktop/portal/desktop"
+SYSTEM_BUS = False
+MAIN_IFACE = "org.freedesktop.impl.portal.Clipboard"
+VERSION = 1
+
+logger = init_template_logger(__name__)
+
+
+def load(mock, parameters=None):
+ logger.debug(f"Loading parameters: {parameters}")
+
+ mock.delay: int = parameters.get("delay", 200)
+ mock.response: int = parameters.get("response", 0)
+ mock.expect_close: bool = parameters.get("expect-close", False)
+ mock.AddProperties(
+ MAIN_IFACE,
+ dbus.Dictionary(
+ {
+ "version": dbus.UInt32(parameters.get("version", VERSION)),
+ }
+ ),
+ )
+
+
+@dbus.service.method(
+ MAIN_IFACE,
+ in_signature="oosa{sv}",
+ out_signature="ua{sv}",
+ async_callbacks=("cb_success", "cb_error"),
+)
+def EnableClipboard(
+ self, handle, session_handle, app_id, options, cb_success, cb_error
+):
+ try:
+ logger.debug(
+ f"EnableClipboard({handle}, {session_handle}, {app_id}, {options})"
+ )
+
+ response = Response(self.response, {})
+
+ request = ImplRequest(self, BUS_NAME, handle)
+
+ if self.expect_close:
+
+ def closed_callback():
+ response = Response(2, {})
+ logger.debug(f"EnableClipboard Close() response {response}")
+ cb_success(response.response, response.results)
+
+ request.export(closed_callback)
+ else:
+ request.export()
+
+ def reply():
+ logger.debug(f"EnableClipboard with response {response}")
+ cb_success(response.response, response.results)
+
+ logger.debug(f"scheduling delay of {self.delay}")
+ GLib.timeout_add(self.delay, reply)
+ except Exception as e:
+ logger.critical(e)
+ cb_error(e)
+
+
+@dbus.service.method(
+ MAIN_IFACE,
+ in_signature="oosa{sv}",
+ out_signature="ua{sv}",
+ async_callbacks=("cb_success", "cb_error"),
+)
+def SetSelection(self, handle, session_handle, app_id, options, cb_success, cb_error):
+ try:
+ logger.debug(
+ f"EnableClipboard({handle}, {session_handle}, {app_id}, {options})"
+ )
+
+ response = Response(self.response, {})
+
+ request = ImplRequest(self, BUS_NAME, handle)
+
+ if self.expect_close:
+
+ def closed_callback():
+ response = Response(2, {})
+ logger.debug(f"SetSelection Close() response {response}")
+ cb_success(response.response, response.results)
+
+ request.export(closed_callback)
+ else:
+ request.export()
+
+ def reply():
+ logger.debug(f"SetSelection with response {response}")
+ cb_success(response.response, response.results)
+
+ logger.debug(f"scheduling delay of {self.delay}")
+ GLib.timeout_add(self.delay, reply)
+ except Exception as e:
+ logger.critical(e)
+ cb_error(e)
+
+
+@dbus.service.method(
+ MAIN_IFACE,
+ in_signature="oosua{sv}",
+ out_signature="ua{sv}",
+ async_callbacks=("cb_success", "cb_error"),
+)
+def SelectionWrite(
+ self, handle, session_handle, app_id, serial, options, cb_success, cb_error
+):
+ try:
+ logger.debug(
+ f"SelectionWrite({handle}, {session_handle}, {app_id}, {serial}, {options})"
+ )
+
+ response = Response(self.response, {})
+
+ request = ImplRequest(self, BUS_NAME, handle)
+
+ # Open a file
+ fd = os.open("foo.txt", os.O_RDWR | os.O_CREAT)
+ response.results["fd"] = dbus.types.UnixFd(fd)
+
+ if self.expect_close:
+
+ def closed_callback():
+ response = Response(2, {})
+ logger.debug(f"SelectionWrite Close() response {response}")
+ cb_success(response.response, response.results)
+
+ request.export(closed_callback)
+ else:
+ request.export()
+
+ def reply():
+ logger.debug(f"SelectionWrite with response {response}")
+ cb_success(response.response, response.results)
+
+ logger.debug(f"scheduling delay of {self.delay}")
+ GLib.timeout_add(self.delay, reply)
+ except Exception as e:
+ logger.critical(e)
+ cb_error(e)
+
+
+@dbus.service.method(
+ MAIN_IFACE,
+ in_signature="oosuba{sv}",
+ out_signature="ua{sv}",
+ async_callbacks=("cb_success", "cb_error"),
+)
+def SelectionWriteDone(
+ self, handle, session_handle, app_id, serial, success, options, cb_success, cb_error
+):
+ try:
+ logger.debug(
+ f"SelectionWriteDone({handle}, {session_handle}, {app_id}, {serial}, {success}, {options})"
+ )
+
+ response = Response(self.response, {})
+
+ request = ImplRequest(self, BUS_NAME, handle)
+
+ if self.expect_close:
+
+ def closed_callback():
+ response = Response(2, {})
+ logger.debug(f"SelectionWriteDone Close() response {response}")
+ cb_success(response.response, response.results)
+
+ request.export(closed_callback)
+ else:
+ request.export()
+
+ def reply():
+ logger.debug(f"SelectionWriteDone with response {response}")
+ cb_success(response.response, response.results)
+
+ logger.debug(f"scheduling delay of {self.delay}")
+ GLib.timeout_add(self.delay, reply)
+ except Exception as e:
+ logger.critical(e)
+ cb_error(e)
+
+
+@dbus.service.method(
+ MAIN_IFACE,
+ in_signature="oossa{sv}",
+ out_signature="ua{sv}",
+ async_callbacks=("cb_success", "cb_error"),
+)
+def SelectionRead(
+ self, handle, session_handle, app_id, mime_type, options, cb_success, cb_error
+):
+ try:
+ logger.debug(
+ f"SelectionRead({handle}, {session_handle}, {app_id}, {mime_type}, {options})"
+ )
+
+ response = Response(self.response, {})
+
+ request = ImplRequest(self, BUS_NAME, handle)
+
+ # Open a file
+ fd = os.open("foo.txt", os.O_RDWR | os.O_CREAT)
+ response.results["fd"] = dbus.types.UnixFd(fd)
+
+ if self.expect_close:
+
+ def closed_callback():
+ response = Response(2, {})
+ logger.debug(f"SelectionRead Close() response {response}")
+ cb_success(response.response, response.results)
+
+ request.export(closed_callback)
+ else:
+ request.export()
+
+ def reply():
+ logger.debug(f"SelectionRead with response {response}")
+ cb_success(response.response, response.results)
+
+ logger.debug(f"scheduling delay of {self.delay}")
+ GLib.timeout_add(self.delay, reply)
+ except Exception as e:
+ logger.critical(e)
+ cb_error(e)
diff --git a/tests/templates/remotedesktop.py b/tests/templates/remotedesktop.py
new file mode 100644
index 000000000..0d4d987eb
--- /dev/null
+++ b/tests/templates/remotedesktop.py
@@ -0,0 +1,69 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is formatted with Python Black
+
+from tests.templates import Response, init_template_logger, ImplRequest
+import dbus.service
+
+from gi.repository import GLib
+
+
+BUS_NAME = "org.freedesktop.impl.portal.Test"
+MAIN_OBJ = "/org/freedesktop/portal/desktop"
+SYSTEM_BUS = False
+MAIN_IFACE = "org.freedesktop.impl.portal.RemoteDesktop"
+VERSION = 1
+
+logger = init_template_logger(__name__)
+
+
+def load(mock, parameters=None):
+ logger.debug(f"Loading parameters: {parameters}")
+
+ mock.delay: int = parameters.get("delay", 200)
+ mock.response: int = parameters.get("response", 0)
+ mock.expect_close: bool = parameters.get("expect-close", False)
+ mock.AddProperties(
+ MAIN_IFACE,
+ dbus.Dictionary(
+ {
+ "version": dbus.UInt32(parameters.get("version", VERSION)),
+ }
+ ),
+ )
+
+
+@dbus.service.method(
+ MAIN_IFACE,
+ in_signature="oosa{sv}",
+ out_signature="ua{sv}",
+ async_callbacks=("cb_success", "cb_error"),
+)
+def CreateSession(self, handle, session_handle, app_id, options, cb_success, cb_error):
+ try:
+ logger.debug(f"CreateSession({handle}, {app_id}, {session_handle}, {options})")
+
+ response = Response(self.response, {})
+
+ request = ImplRequest(self, BUS_NAME, handle)
+
+ if self.expect_close:
+
+ def closed_callback():
+ response = Response(2, {})
+ logger.debug(f"CreateSession Close() response {response}")
+ cb_success(response.response, response.results)
+
+ request.export(closed_callback)
+ else:
+ request.export()
+
+ def reply():
+ logger.debug(f"CreateSession with response {response}")
+ cb_success(response.response, response.results)
+
+ logger.debug(f"scheduling delay of {self.delay}")
+ GLib.timeout_add(self.delay, reply)
+ except Exception as e:
+ logger.critical(e)
+ cb_error(e)
diff --git a/tests/test_clipboard.py b/tests/test_clipboard.py
new file mode 100644
index 000000000..ac59b9e2a
--- /dev/null
+++ b/tests/test_clipboard.py
@@ -0,0 +1,93 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is formatted with Python Black
+
+from logging import debug
+from tests import Request, PortalTest
+from gi.repository import GLib
+import dbus
+
+
+class TestClipboard(PortalTest):
+ def test_version(self):
+ self.check_version(1)
+
+ def enable_clipboard(self, session_handle):
+ clipboard_interface = self.get_dbus_interface()
+ enable_clipboard_request = Request(self.dbus_con, clipboard_interface)
+ enable_clipboard_response = enable_clipboard_request.call(
+ "EnableClipboard", session_handle=session_handle, options={}
+ )
+ assert enable_clipboard_response.response == 0
+
+ def create_session_handle(self, enableClipboard=True):
+ self.start_xdp()
+ self.start_impl_portal()
+ self.add_template("RemoteDesktop")
+
+ remote_desktop_interface = self.get_dbus_interface("RemoteDesktop")
+
+ create_session_request = Request(self.dbus_con, remote_desktop_interface)
+ create_session_response = create_session_request.call(
+ "CreateSession", options={"session_handle_token": "1234"}
+ )
+ assert create_session_response.response == 0
+ assert str(create_session_response.results["session_handle"])
+
+ session_handle = create_session_response.results["session_handle"]
+
+ if enableClipboard:
+ self.enable_clipboard(session_handle)
+
+ return create_session_response.results["session_handle"]
+
+ def test_clipboard_set_selection(self):
+ session_handle = self.create_session_handle()
+ clipboard_interface = self.get_dbus_interface()
+
+ set_selection_request = Request(self.dbus_con, clipboard_interface)
+ set_selection_response = set_selection_request.call(
+ "SetSelection", session_handle=session_handle, options={}
+ )
+ assert set_selection_response.response == 0
+
+ def test_clipboard_selection_write(self):
+ session_handle = self.create_session_handle()
+ clipboard_interface = self.get_dbus_interface()
+
+ selection_write_request = Request(self.dbus_con, clipboard_interface)
+ selection_write_response = selection_write_request.call(
+ "SelectionWrite", session_handle=session_handle, serial=4321, options={}
+ )
+ assert selection_write_response.response == 0
+
+ selection_write_done_request = Request(self.dbus_con, clipboard_interface)
+ selection_write_done_response = selection_write_done_request.call(
+ "SelectionWriteDone",
+ session_handle=session_handle,
+ serial=4321,
+ success=True,
+ options={},
+ )
+
+ assert selection_write_done_response.response == 0
+
+ def test_clipboard_selection_read(self):
+ session_handle = self.create_session_handle()
+ clipboard_interface = self.get_dbus_interface()
+
+ selection_read_request = Request(self.dbus_con, clipboard_interface)
+ selection_read_response = selection_read_request.call(
+ "SelectionRead", session_handle=session_handle, mime_type="", options={}
+ )
+ assert selection_read_response.response == 0
+
+ def test_clipboard_throws_without_enable(self):
+ session_handle = self.create_session_handle(False) # clipboard not enabled
+ clipboard_interface = self.get_dbus_interface()
+
+ set_selection_request = Request(self.dbus_con, clipboard_interface)
+ with self.assertRaises(dbus.exceptions.DBusException):
+ set_selection_request.call(
+ "SetSelection", session_handle=session_handle, options={}
+ )