File 004-Implement-client-to-server-clipboard-update-notification.patch of Package gtk-vnc

Subject: Implement client-to-server clipboard update notification
From: Lin Ma lma@suse.de Mon Jul 7 12:59:03 2025 +0800
Date: Sun Oct 12 16:55:56 2025 +0800:
Git: 348452c1007bbfd3575b94263d9e55b0e11b79db

This patch implements the mechanism for notifying the VNC server side
when the client's local clipboard content has changed.

On the VncDisplay (UI) layer:
- It detects local clipboard changes by listening to the 'owner-change'
  signal on the GDK 'PRIMARY' selection.
- A 50ms timer is used to debounce events, filtering out transient,
  invalid events caused by actions like double-clicks.
- When valid clipboard content is confirmed, it calls the underlying
  VncConnection function.

On the VncConnection (protocol) layer:
- A new public function vnc_connection_handle_clipboard_change, is added
  to serve as the entry point for triggering clipboard notifications from
  the UI layer.
- The vnc_connection_write_clipboard_notify function is implemented to
  construct and send an extended clipboard message with the NOTIFY action.

This patch completes the "announcement" phase of a client-side clipboard
update, laying the groundwork for the server to subsequently request the
data.

Signed-off-by: Lin Ma <lma@suse.de>

--- a/src/libgvnc_sym.version
+++ b/src/libgvnc_sym.version
@@ -95,6 +95,9 @@
 	vnc_connection_power_control;
 	vnc_connection_set_size;
 
+	vnc_connection_handle_clipboard_change;
+	vnc_connection_clear_pending_flag;
+
 	vnc_util_set_debug;
 	vnc_util_get_debug;
 	vnc_util_get_version;
--- a/src/vncconnection.c
+++ b/src/vncconnection.c
@@ -147,6 +147,10 @@ static void vnc_connection_handle_clipbo
                                                  const guint32 *lengths);
 static void vnc_connection_handle_extended_clipboard(VncConnection *conn,
                                                      gint32 len);
+static void vnc_connection_write_clipboard_notify(VncConnection *conn,
+                                                  guint32 flags);
+static void vnc_connection_announce_clipboard(VncConnection *conn,
+                                              gboolean available);
 
 /*
  * A special GSource impl which allows us to wait on a certain
@@ -276,6 +280,7 @@ struct _VncConnectionPrivate
     gboolean has_extended_clipboard;
     guint32 server_clipboard_flags;
     guint32 server_clipboard_sizes[VNC_CLIPBOARD_MAX_FORMATS];
+    gboolean pendingClientClipboard;
 };
 
 G_DEFINE_TYPE_WITH_PRIVATE(VncConnection, vnc_connection, G_TYPE_OBJECT);
@@ -5619,6 +5624,7 @@ static void vnc_connection_init(VncConne
     priv->has_extended_clipboard = FALSE;
     priv->server_clipboard_flags = 0;
     memset(priv->server_clipboard_sizes, 0, sizeof(priv->server_clipboard_sizes));
+    priv->pendingClientClipboard = FALSE;
 }
 
 
@@ -6926,3 +6932,49 @@ vnc_connection_handle_extended_clipboard
 
     return;
 }
+
+static void vnc_connection_write_clipboard_notify(VncConnection *conn,
+                                                  guint32 flags)
+{
+    guint8 pad[3] = {0};
+
+    vnc_connection_buffered_write_u8(conn, VNC_CONNECTION_CLIENT_MESSAGE_CUT_TEXT);
+    vnc_connection_buffered_write(conn, pad, 3);
+
+    /* Payload is flags(4). Negative length indicates extended message. */
+    vnc_connection_buffered_write_s32(conn, -4);
+
+    vnc_connection_buffered_write_u32(conn, flags | VNC_CLIPBOARD_ACTION_NOTIFY);
+
+    vnc_connection_buffered_flush(conn);
+}
+
+static void vnc_connection_announce_clipboard(VncConnection *conn,
+                                              gboolean available)
+{
+
+    if (!(conn->priv->server_clipboard_flags & VNC_CLIPBOARD_ACTION_NOTIFY)) {
+        vnc_connection_set_error(conn,
+                                 "Server does not support clipboard "
+                                 "'notify' action");
+    }
+
+    guint32 flags = available ? VNC_CLIPBOARD_FORMAT_UTF8 : 0;
+    vnc_connection_write_clipboard_notify(conn, flags);
+}
+
+void vnc_connection_handle_clipboard_change(VncConnection *conn)
+{
+    VncConnectionPrivate *priv = conn->priv;
+
+    priv->pendingClientClipboard = TRUE;
+
+    vnc_connection_announce_clipboard(conn, FALSE);
+}
+
+void vnc_connection_clear_pending_flag(VncConnection *conn)
+{
+    VncConnectionPrivate *priv = conn->priv;
+
+    priv->pendingClientClipboard = FALSE;
+}
--- a/src/vncconnection.h
+++ b/src/vncconnection.h
@@ -246,6 +246,9 @@ gboolean vnc_connection_has_error(VncCon
 gboolean vnc_connection_set_framebuffer(VncConnection *conn,
                                         VncFramebuffer *fb);
 
+void vnc_connection_handle_clipboard_change(VncConnection *conn);
+void vnc_connection_clear_pending_flag(VncConnection *conn);
+
 const char *vnc_connection_get_name(VncConnection *conn);
 int vnc_connection_get_width(VncConnection *conn);
 int vnc_connection_get_height(VncConnection *conn);
--- a/src/vncdisplay.c
+++ b/src/vncdisplay.c
@@ -100,6 +100,8 @@ struct _VncDisplayPrivate
     VncGrabSequence *vncgrabseq; /* the configured key sequence */
     gboolean *vncactiveseq; /* the currently pressed keys */
 
+    guint primary_selection_timer_id;
+
 #ifdef WIN32
     HHOOK keyboard_hook;
 #endif
@@ -1826,6 +1828,54 @@ static void on_auth_unsupported(VncConne
     g_signal_emit(G_OBJECT(obj), signals[VNC_AUTH_UNSUPPORTED], 0, authType);
 }
 
+static void handle_primary_received(GtkClipboard *clipboard  G_GNUC_UNUSED,
+                                    const gchar *text,
+                                    gpointer opaque)
+{
+    VncDisplay *display = VNC_DISPLAY(opaque);
+    VncDisplayPrivate *priv = display->priv;
+
+    if (text != NULL && *text != '\0' && !priv->read_only) {
+        vnc_connection_handle_clipboard_change(priv->conn);
+    } else {
+        vnc_connection_clear_pending_flag(priv->conn);
+    }
+}
+
+static gboolean handle_primary_request(gpointer opaque)
+{
+    VncDisplay *display = VNC_DISPLAY(opaque);
+    VncDisplayPrivate *priv = display->priv;
+
+    GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
+    gtk_clipboard_request_text(clipboard, handle_primary_received, opaque);
+
+    /* Returns FALSE to indicate that this timer will be executed only once and
+     * then automatically destroyed. */
+    priv->primary_selection_timer_id = 0;
+    return G_SOURCE_REMOVE;
+}
+
+static void on_primary_owner_change(GtkClipboard *clipboard G_GNUC_UNUSED,
+                                    GdkEventOwnerChange *event G_GNUC_UNUSED,
+                                    gpointer opaque)
+{
+    VncDisplay *display = VNC_DISPLAY(opaque);
+    VncDisplayPrivate *priv = display->priv;
+
+    /* If a timer is already running, cancel it first. */
+    if (priv->primary_selection_timer_id != 0) {
+        g_source_remove(priv->primary_selection_timer_id);
+    }
+
+    /* Start a new timer and execute the real request after 50 milliseconds,
+     * This delay is enough to filter out intermediate events generated by
+     * double clicks */
+    priv->primary_selection_timer_id = g_timeout_add(50,
+                                                     handle_primary_request,
+                                                     opaque);
+}
+
 static void on_server_cut_text(VncConnection *conn G_GNUC_UNUSED,
                                const gchar *text,
                                gpointer opaque)
@@ -3003,6 +3053,8 @@ static void vnc_display_init(VncDisplay
                      G_CALLBACK(on_power_control_init), display);
     g_signal_connect(G_OBJECT(priv->conn), "vnc-power-control-failed",
                      G_CALLBACK(on_power_control_fail), display);
+    g_signal_connect(gtk_clipboard_get(GDK_SELECTION_PRIMARY), "owner-change",
+                     G_CALLBACK(on_primary_owner_change), display);
 
     priv->keycode_map = vnc_display_keymap_gdk2rfb_table(&priv->keycode_maplen);
 }
openSUSE Build Service is sponsored by