Compare commits
2 Commits
| Author | SHA256 | Date | |
|---|---|---|---|
| 48a978b07f | |||
| 9dc609fc34 |
69
001-src-introduce-a-vncclipboard.h-header-file.patch
Normal file
69
001-src-introduce-a-vncclipboard.h-header-file.patch
Normal file
@@ -0,0 +1,69 @@
|
||||
Subject: src: introduce a vncclipboard.h header file
|
||||
From: Lin Ma lma@suse.de Mon Jul 7 12:39:46 2025 +0800
|
||||
Date: Sun Oct 12 16:55:56 2025 +0800:
|
||||
Git: 4806aa5889ea50508ef25347641c42acc601dd31
|
||||
|
||||
This header file defines the formats and the actions agreed upon by the
|
||||
RFB extended clipboard protocol.
|
||||
|
||||
Signed-off-by: Lin Ma <lma@suse.de>
|
||||
|
||||
diff --git a/src/meson.build b/src/meson.build
|
||||
index f5a11cf..941fe70 100644
|
||||
--- a/src/meson.build
|
||||
+++ b/src/meson.build
|
||||
@@ -58,6 +58,7 @@ gvnc_headers = [
|
||||
'vnccolormap.h',
|
||||
'vncconnection.h',
|
||||
'vncutil.h',
|
||||
+ 'vncclipboard.h',
|
||||
]
|
||||
|
||||
install_headers(gvnc_headers + [gvnc_version_header], subdir: 'gvnc-1.0')
|
||||
diff --git a/src/vncclipboard.h b/src/vncclipboard.h
|
||||
new file mode 100644
|
||||
index 0000000..dd59bf4
|
||||
--- /dev/null
|
||||
+++ b/src/vncclipboard.h
|
||||
@@ -0,0 +1,41 @@
|
||||
+/*
|
||||
+ * GTK VNC Widget
|
||||
+ *
|
||||
+ * This library 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.0 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, write to the Free Software
|
||||
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
+ */
|
||||
+
|
||||
+#ifndef VNC_CLIPBOARD_H
|
||||
+#define VNC_CLIPBOARD_H
|
||||
+
|
||||
+/* Max number of extended clipboard formats we might track(bits 0-15) */
|
||||
+#define VNC_CLIPBOARD_MAX_FORMATS 16
|
||||
+
|
||||
+/* Extended Clipboard Formats */
|
||||
+#define VNC_CLIPBOARD_FORMAT_UTF8 (1 << 0)
|
||||
+#define VNC_CLIPBOARD_FORMAT_RTF (1 << 1)
|
||||
+#define VNC_CLIPBOARD_FORMAT_HTML (1 << 2)
|
||||
+#define VNC_CLIPBOARD_FORMAT_DIB (1 << 3)
|
||||
+#define VNC_CLIPBOARD_FORMAT_FILES (1 << 4)
|
||||
+
|
||||
+/* Extended Clipboard Actions */
|
||||
+#define VNC_CLIPBOARD_ACTION_CAPS (1 << 24)
|
||||
+#define VNC_CLIPBOARD_ACTION_REQUEST (1 << 25)
|
||||
+#define VNC_CLIPBOARD_ACTION_PEEK (1 << 26)
|
||||
+#define VNC_CLIPBOARD_ACTION_NOTIFY (1 << 27)
|
||||
+#define VNC_CLIPBOARD_ACTION_PROVIDE (1 << 28)
|
||||
+
|
||||
+#define VNC_CLIPBOARD_ACTION_MASK 0xff000000
|
||||
+
|
||||
+#endif /* VNC_CLIPBOARD_H */
|
||||
36
002-Add-the-extended-clipboard-pseudo-encoding.patch
Normal file
36
002-Add-the-extended-clipboard-pseudo-encoding.patch
Normal file
@@ -0,0 +1,36 @@
|
||||
Subject: Add the extended clipboard pseudo-encoding
|
||||
From: Lin Ma lma@suse.de Mon Jul 7 12:40:53 2025 +0800
|
||||
Date: Sun Oct 12 16:55:56 2025 +0800:
|
||||
Git: e334dd438907251d7b42615824d9c14ae3452331
|
||||
|
||||
Signed-off-by: Lin Ma <lma@suse.de>
|
||||
|
||||
--- a/src/vncconnection.h
|
||||
+++ b/src/vncconnection.h
|
||||
@@ -131,6 +131,7 @@ typedef enum {
|
||||
VNC_CONNECTION_ENCODING_EXTENDED_DESKTOP_RESIZE = -308,
|
||||
VNC_CONNECTION_ENCODING_XVP = -309,
|
||||
VNC_CONNECTION_ENCODING_ALPHA_CURSOR = -314,
|
||||
+ VNC_CONNECTION_ENCODING_EXTENDED_CLIPBOARD = 0xC0A1E5CE,
|
||||
} VncConnectionEncoding;
|
||||
|
||||
/**
|
||||
--- a/src/vncdisplay.c
|
||||
+++ b/src/vncdisplay.c
|
||||
@@ -1973,6 +1973,7 @@ static void on_initialized(VncConnection
|
||||
VNC_CONNECTION_ENCODING_EXTENDED_DESKTOP_RESIZE,
|
||||
VNC_CONNECTION_ENCODING_DESKTOP_RESIZE,
|
||||
VNC_CONNECTION_ENCODING_DESKTOP_NAME,
|
||||
+ VNC_CONNECTION_ENCODING_EXTENDED_CLIPBOARD,
|
||||
VNC_CONNECTION_ENCODING_LAST_RECT,
|
||||
VNC_CONNECTION_ENCODING_WMVi,
|
||||
VNC_CONNECTION_ENCODING_AUDIO,
|
||||
@@ -1996,7 +1997,7 @@ static void on_initialized(VncConnection
|
||||
sizeof(gint32) * \
|
||||
(n_encodings - (i + 1))); \
|
||||
n_encodings--; \
|
||||
- VNC_DEBUG("Removed encoding %d", e); \
|
||||
+ VNC_DEBUG("Removed encoding %d", (int)e); \
|
||||
break; \
|
||||
} \
|
||||
}
|
||||
263
003-Implement-extended-clipboard-capability-negotiation.patch
Normal file
263
003-Implement-extended-clipboard-capability-negotiation.patch
Normal file
@@ -0,0 +1,263 @@
|
||||
Subject: Implement extended clipboard capability negotiation
|
||||
From: Lin Ma lma@suse.de Mon Jul 7 12:41:10 2025 +0800
|
||||
Date: Sun Oct 12 16:55:56 2025 +0800:
|
||||
Git: 1c813530f80ddd58029b0c2dca5228f205c50a9f
|
||||
|
||||
This patch implements the initial capability negotiation for the RFB
|
||||
Extended Clipboard protocol.
|
||||
|
||||
It modifies the vnc_connection_server_message to detect the extended
|
||||
ServerCutText messages, which are identified by a negative length
|
||||
value.
|
||||
|
||||
Upon receiving a message with the VNC_CLIPBOARD_ACTION_CAPS flag, The
|
||||
vnc_connection_handle_clipboard_caps function is called. This function
|
||||
parses the list of formats and their maximum sizes advertised by the
|
||||
server and stores them in the private connection state.
|
||||
|
||||
After processing the server's capabilities, Client responds by sending
|
||||
its own capabilities back. The vnc_connection_write_clipboard_caps
|
||||
function constructs this response, announcing the client's supported
|
||||
formats and actions.
|
||||
|
||||
New fields have been added to the VncConnectionPrivate struct to track
|
||||
the negotiation state and the server's capabilities.
|
||||
|
||||
This commit establishes the foundational handshake required for all
|
||||
subsequent extended clipboard operations.
|
||||
|
||||
Signed-off-by: Lin Ma <lma@suse.de>
|
||||
|
||||
--- a/src/vncconnection.c
|
||||
+++ b/src/vncconnection.c
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "vncmarshal.h"
|
||||
#include "vncbaseframebuffer.h"
|
||||
#include "vncutil.h"
|
||||
+#include "vncclipboard.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
@@ -58,6 +59,11 @@
|
||||
|
||||
#define GTK_VNC_ERROR g_quark_from_static_string("gtk-vnc")
|
||||
|
||||
+/* A reasonable upper limit for an extended clipboard message, which
|
||||
+ * contains metadata, not the actual clipboard content.
|
||||
+ */
|
||||
+#define VNC_MAX_EXTENDED_CLIPBOARD_MSG_SIZE (256 * 1024)
|
||||
+
|
||||
struct wait_queue
|
||||
{
|
||||
gboolean waiting;
|
||||
@@ -133,6 +139,15 @@ static void vnc_connection_set_error(Vnc
|
||||
...) G_GNUC_PRINTF(2, 3);
|
||||
static void vnc_connection_auth_failure(VncConnection *conn, const char *reason);
|
||||
|
||||
+static void vnc_connection_write_clipboard_caps(VncConnection *conn,
|
||||
+ guint32 caps,
|
||||
+ const guint32 *lengths);
|
||||
+static void vnc_connection_handle_clipboard_caps(VncConnection *conn,
|
||||
+ guint32 flags,
|
||||
+ const guint32 *lengths);
|
||||
+static void vnc_connection_handle_extended_clipboard(VncConnection *conn,
|
||||
+ gint32 len);
|
||||
+
|
||||
/*
|
||||
* A special GSource impl which allows us to wait on a certain
|
||||
* condition to be satisfied. This is effectively a boolean test
|
||||
@@ -257,6 +272,10 @@ struct _VncConnectionPrivate
|
||||
VncAudio *audio;
|
||||
VncAudioSample *audio_sample;
|
||||
guint audio_timer;
|
||||
+
|
||||
+ gboolean has_extended_clipboard;
|
||||
+ guint32 server_clipboard_flags;
|
||||
+ guint32 server_clipboard_sizes[VNC_CLIPBOARD_MAX_FORMATS];
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE_WITH_PRIVATE(VncConnection, vnc_connection, G_TYPE_OBJECT);
|
||||
@@ -3670,23 +3689,24 @@ static gboolean vnc_connection_server_me
|
||||
break;
|
||||
case VNC_CONNECTION_SERVER_MESSAGE_SERVER_CUT_TEXT: {
|
||||
guint8 pad[3];
|
||||
- guint32 n_text;
|
||||
- char *data;
|
||||
-
|
||||
+ guint32 len;
|
||||
vnc_connection_read(conn, pad, 3);
|
||||
- n_text = vnc_connection_read_u32(conn);
|
||||
- if (n_text > (32 << 20)) {
|
||||
- vnc_connection_set_error(conn, "Cut text length %u longer than permitted %d",
|
||||
- n_text, (32 << 20));
|
||||
- break;
|
||||
- }
|
||||
-
|
||||
- data = g_new(char, n_text + 1);
|
||||
- vnc_connection_read(conn, data, n_text);
|
||||
- data[n_text] = 0;
|
||||
+ len = vnc_connection_read_u32(conn);
|
||||
|
||||
- vnc_connection_server_cut_text(conn, data, n_text);
|
||||
- g_free(data);
|
||||
+ if ((gint32)len < 0) {
|
||||
+ vnc_connection_handle_extended_clipboard(conn, -(gint32)len);
|
||||
+ } else {
|
||||
+ if (len > (32 << 20)) {
|
||||
+ vnc_connection_set_error(conn, "Cut text length %u longer than permitted %d",
|
||||
+ len, (32 << 20));
|
||||
+ break;
|
||||
+ }
|
||||
+ char *data = g_new(char, len + 1);
|
||||
+ vnc_connection_read(conn, data, len);
|
||||
+ data[len] = 0;
|
||||
+ vnc_connection_server_cut_text(conn, data, len);
|
||||
+ g_free(data);
|
||||
+ }
|
||||
} break;
|
||||
case VNC_CONNECTION_SERVER_MESSAGE_XVP: {
|
||||
guint8 pad[1];
|
||||
@@ -5595,6 +5615,10 @@ static void vnc_connection_init(VncConne
|
||||
priv->fd = -1;
|
||||
priv->auth_type = VNC_CONNECTION_AUTH_INVALID;
|
||||
priv->auth_subtype = VNC_CONNECTION_AUTH_INVALID;
|
||||
+
|
||||
+ priv->has_extended_clipboard = FALSE;
|
||||
+ priv->server_clipboard_flags = 0;
|
||||
+ memset(priv->server_clipboard_sizes, 0, sizeof(priv->server_clipboard_sizes));
|
||||
}
|
||||
|
||||
|
||||
@@ -6773,3 +6797,132 @@ VncConnectionResizeStatus vnc_connection
|
||||
return !vnc_connection_has_error(conn);
|
||||
|
||||
}
|
||||
+
|
||||
+static void
|
||||
+vnc_connection_handle_clipboard_caps(VncConnection *conn,
|
||||
+ guint32 flags,
|
||||
+ const guint32 *lengths)
|
||||
+{
|
||||
+ VncConnectionPrivate *priv = conn->priv;
|
||||
+ int i, num = 0;
|
||||
+ guint32 client_caps;
|
||||
+ guint32 sizes[] = {0};
|
||||
+
|
||||
+ VNC_DEBUG("Got server clipboard capabilities (flags 0x%x)", flags);
|
||||
+
|
||||
+ priv->has_extended_clipboard = TRUE;
|
||||
+ priv->server_clipboard_flags = flags;
|
||||
+
|
||||
+ memset(priv->server_clipboard_sizes, 0, sizeof(priv->server_clipboard_sizes));
|
||||
+ for (i = 0; i < VNC_CLIPBOARD_MAX_FORMATS; i++) {
|
||||
+ if (flags & (1 << i)) {
|
||||
+ priv->server_clipboard_sizes[i] = lengths[num++];
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Announce our capabilities, Currently only implements UTF8 format handling.
|
||||
+ client_caps = (VNC_CLIPBOARD_FORMAT_UTF8 |
|
||||
+ VNC_CLIPBOARD_ACTION_REQUEST |
|
||||
+ VNC_CLIPBOARD_ACTION_PEEK |
|
||||
+ VNC_CLIPBOARD_ACTION_NOTIFY |
|
||||
+ VNC_CLIPBOARD_ACTION_PROVIDE);
|
||||
+
|
||||
+ vnc_connection_write_clipboard_caps(conn, client_caps, sizes);
|
||||
+}
|
||||
+
|
||||
+static void
|
||||
+vnc_connection_write_clipboard_caps(VncConnection *conn,
|
||||
+ guint32 caps,
|
||||
+ const guint32 *lengths)
|
||||
+{
|
||||
+ int i, count = 0;
|
||||
+ guint8 pad[3] = {0};
|
||||
+
|
||||
+ VNC_DEBUG("Sending client clipboard capabilities (caps 0x%x)", caps);
|
||||
+
|
||||
+ vnc_connection_buffered_write_u8(conn, VNC_CONNECTION_CLIENT_MESSAGE_CUT_TEXT);
|
||||
+ vnc_connection_buffered_write(conn, pad, 3);
|
||||
+
|
||||
+ for (i = 0; i < VNC_CLIPBOARD_MAX_FORMATS; i++) {
|
||||
+ if (caps & (1 << i))
|
||||
+ count++;
|
||||
+ }
|
||||
+
|
||||
+ // Payload is flags(4) + one size per format
|
||||
+ vnc_connection_buffered_write_s32(conn, -(4 + 4 * count));
|
||||
+
|
||||
+ vnc_connection_buffered_write_u32(conn, caps | VNC_CLIPBOARD_ACTION_CAPS);
|
||||
+
|
||||
+ count = 0;
|
||||
+ for (i = 0; i < VNC_CLIPBOARD_MAX_FORMATS; i++) {
|
||||
+ if (caps & (1 << i))
|
||||
+ vnc_connection_buffered_write_u32(conn, lengths[count++]);
|
||||
+ }
|
||||
+
|
||||
+ vnc_connection_buffered_flush(conn);
|
||||
+}
|
||||
+
|
||||
+static void
|
||||
+vnc_connection_handle_extended_clipboard(VncConnection *conn, gint32 len)
|
||||
+{
|
||||
+ guint32 flags, action;
|
||||
+
|
||||
+ if (len < 4) {
|
||||
+ vnc_connection_set_error(conn,
|
||||
+ "Invalid extended clipboard message: length "
|
||||
+ "%d is less than 4",
|
||||
+ len);
|
||||
+ return;
|
||||
+ }
|
||||
+ if (len > VNC_MAX_EXTENDED_CLIPBOARD_MSG_SIZE) {
|
||||
+ vnc_connection_set_error(conn,
|
||||
+ "Extended clipboard message too long (%d bytes).",
|
||||
+ len);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ flags = vnc_connection_read_u32(conn);
|
||||
+ if (vnc_connection_has_error(conn))
|
||||
+ return;
|
||||
+
|
||||
+ action = flags & VNC_CLIPBOARD_ACTION_MASK;
|
||||
+
|
||||
+ if (action & VNC_CLIPBOARD_ACTION_CAPS) {
|
||||
+ int i, format_count = 0;
|
||||
+ guint32 lengths[VNC_CLIPBOARD_MAX_FORMATS] = {0};
|
||||
+
|
||||
+ for (i = 0; i < VNC_CLIPBOARD_MAX_FORMATS; i++) {
|
||||
+ if (flags & (1 << i))
|
||||
+ format_count++;
|
||||
+ }
|
||||
+
|
||||
+ if (len < (4 + 4 * format_count)) {
|
||||
+ vnc_connection_set_error(conn,
|
||||
+ "Invalid clipboard caps message, length "
|
||||
+ "%d is too small for %d formats",
|
||||
+ len, format_count);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ for (i = 0; i < format_count; i++) {
|
||||
+ lengths[i] = vnc_connection_read_u32(conn);
|
||||
+ if (vnc_connection_has_error(conn))
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ vnc_connection_handle_clipboard_caps(conn, flags, lengths);
|
||||
+
|
||||
+ } else if (action == VNC_CLIPBOARD_ACTION_PROVIDE) {
|
||||
+ VNC_DEBUG("Received clipboard action 'provide', not implemented yet");
|
||||
+ } else if (action == VNC_CLIPBOARD_ACTION_REQUEST) {
|
||||
+ VNC_DEBUG("Received clipboard action 'request', not implemented yet");
|
||||
+ } else if (action == VNC_CLIPBOARD_ACTION_PEEK) {
|
||||
+ VNC_DEBUG("Received clipboard action 'peek', not implemented yet");
|
||||
+ } else if (action == VNC_CLIPBOARD_ACTION_NOTIFY) {
|
||||
+ VNC_DEBUG("Received clipboard action 'notify', not implemented yet");
|
||||
+ } else {
|
||||
+ VNC_DEBUG("Unknown extended clipboard action 0x%x", action);
|
||||
+ }
|
||||
+
|
||||
+ return;
|
||||
+}
|
||||
@@ -0,0 +1,207 @@
|
||||
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);
|
||||
}
|
||||
93
005-Flush-pending-clipboard-on-focus-in-event.patch
Normal file
93
005-Flush-pending-clipboard-on-focus-in-event.patch
Normal file
@@ -0,0 +1,93 @@
|
||||
Subject: Flush pending clipboard on focus-in event
|
||||
From: Lin Ma lma@suse.de Mon Jul 7 13:00:46 2025 +0800
|
||||
Date: Sun Oct 12 16:55:56 2025 +0800:
|
||||
Git: 6c8d86919878f08665e3c92915c58c64901af3ea
|
||||
|
||||
This patch introduces a mechanism to handle clipboard synchronization
|
||||
when the VNC client window regains focus.
|
||||
|
||||
Previously, if a user copied content while the VNC window was inactive,
|
||||
the clipboard change state 'pendingClientClipboard' was recorded but not
|
||||
immediately notified to the server.
|
||||
|
||||
This commit addresses this by:
|
||||
- Making VncDisplay listen for the 'focus-in-event' signal.
|
||||
- When the widget gains focus, the vnc_connection_flush_pending_clipboard
|
||||
function is called.
|
||||
- This function checks for a pending clipboard update and, if one exists,
|
||||
immediately sends a NOTIFY message to the server side, announcing that
|
||||
clipboard content is available.
|
||||
|
||||
This "lazy synchronization" strategy ensures that the clipboard state is
|
||||
updated only when the user re-engages with the VNC session.
|
||||
|
||||
Signed-off-by: Lin Ma <lma@suse.de>
|
||||
|
||||
--- a/src/libgvnc_sym.version
|
||||
+++ b/src/libgvnc_sym.version
|
||||
@@ -97,6 +97,7 @@
|
||||
|
||||
vnc_connection_handle_clipboard_change;
|
||||
vnc_connection_clear_pending_flag;
|
||||
+ vnc_connection_flush_pending_clipboard;
|
||||
|
||||
vnc_util_set_debug;
|
||||
vnc_util_get_debug;
|
||||
--- a/src/vncconnection.c
|
||||
+++ b/src/vncconnection.c
|
||||
@@ -6978,3 +6978,12 @@ void vnc_connection_clear_pending_flag(V
|
||||
|
||||
priv->pendingClientClipboard = FALSE;
|
||||
}
|
||||
+
|
||||
+void vnc_connection_flush_pending_clipboard(VncConnection *conn)
|
||||
+{
|
||||
+ VncConnectionPrivate *priv = conn->priv;
|
||||
+
|
||||
+ if (priv->pendingClientClipboard)
|
||||
+ vnc_connection_announce_clipboard(conn, TRUE);
|
||||
+ priv->pendingClientClipboard = FALSE;
|
||||
+}
|
||||
--- a/src/vncconnection.h
|
||||
+++ b/src/vncconnection.h
|
||||
@@ -248,6 +248,7 @@ gboolean vnc_connection_set_framebuffer(
|
||||
|
||||
void vnc_connection_handle_clipboard_change(VncConnection *conn);
|
||||
void vnc_connection_clear_pending_flag(VncConnection *conn);
|
||||
+void vnc_connection_flush_pending_clipboard(VncConnection *conn);
|
||||
|
||||
const char *vnc_connection_get_name(VncConnection *conn);
|
||||
int vnc_connection_get_width(VncConnection *conn);
|
||||
--- a/src/vncdisplay.c
|
||||
+++ b/src/vncdisplay.c
|
||||
@@ -1876,6 +1876,21 @@ static void on_primary_owner_change(GtkC
|
||||
opaque);
|
||||
}
|
||||
|
||||
+static gboolean on_focus_in(GtkWidget *widget G_GNUC_UNUSED,
|
||||
+ GdkEventFocus *event G_GNUC_UNUSED,
|
||||
+ gpointer opaqueg)
|
||||
+{
|
||||
+ VncDisplay *display = VNC_DISPLAY(opaqueg);
|
||||
+ VncDisplayPrivate *priv = display->priv;
|
||||
+
|
||||
+ VNC_DEBUG("VncDisplay gained focus.");
|
||||
+
|
||||
+ if (!priv->read_only)
|
||||
+ vnc_connection_flush_pending_clipboard(priv->conn);
|
||||
+
|
||||
+ return FALSE;
|
||||
+}
|
||||
+
|
||||
static void on_server_cut_text(VncConnection *conn G_GNUC_UNUSED,
|
||||
const gchar *text,
|
||||
gpointer opaque)
|
||||
@@ -3055,6 +3070,8 @@ static void vnc_display_init(VncDisplay
|
||||
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);
|
||||
+ g_signal_connect(display, "focus-in-event",
|
||||
+ G_CALLBACK(on_focus_in), display);
|
||||
|
||||
priv->keycode_map = vnc_display_keymap_gdk2rfb_table(&priv->keycode_maplen);
|
||||
}
|
||||
272
006-Implement-response-to-server-clipboard-REQUEST-action.patch
Normal file
272
006-Implement-response-to-server-clipboard-REQUEST-action.patch
Normal file
@@ -0,0 +1,272 @@
|
||||
Subject: Implement response to server clipboard REQUEST action
|
||||
From: Lin Ma lma@suse.de Mon Jul 7 13:05:22 2025 +0800
|
||||
Date: Sun Oct 12 16:55:56 2025 +0800:
|
||||
Git: e5ecf1243b7b7ac274681567ed9bb1b596967b28
|
||||
|
||||
This patch implements the full logic for the client to handle a server's
|
||||
VNC_CLIPBOARD_ACTION_REQUEST and send back the clipboard data.
|
||||
|
||||
When the VncConnection receives a data request from the server, it now:
|
||||
1. Emits a new GObject signal, 'clipboard-request', to propagate the
|
||||
protocol event to the upper VncDisplay layer.
|
||||
2. VncDisplay, upon catching this signal, asynchronously requests the
|
||||
text content of the 'PRIMARY' selection from GTK.
|
||||
3. After successfully fetching the text, it passes the data back to the
|
||||
VncConnection via the public API vnc_connection_send_clipboard_data.
|
||||
4. The VncConnection then compresses the data using zlib and sends it
|
||||
to the server side.
|
||||
|
||||
This implementation handles the decoupling of the protocol and UI layers,
|
||||
manages GTK's asynchronous clipboard operations, and completes the
|
||||
request-response loop for client-to-server data synchronization.
|
||||
|
||||
Signed-off-by: Lin Ma <lma@suse.de>
|
||||
|
||||
--- a/src/libgvnc_sym.version
|
||||
+++ b/src/libgvnc_sym.version
|
||||
@@ -98,6 +98,7 @@
|
||||
vnc_connection_handle_clipboard_change;
|
||||
vnc_connection_clear_pending_flag;
|
||||
vnc_connection_flush_pending_clipboard;
|
||||
+ vnc_connection_send_clipboard_data;
|
||||
|
||||
vnc_util_set_debug;
|
||||
vnc_util_get_debug;
|
||||
--- a/src/vncconnection.c
|
||||
+++ b/src/vncconnection.c
|
||||
@@ -151,6 +151,10 @@ static void vnc_connection_write_clipboa
|
||||
guint32 flags);
|
||||
static void vnc_connection_announce_clipboard(VncConnection *conn,
|
||||
gboolean available);
|
||||
+static void vnc_connection_write_clipboard_provide(VncConnection *conn,
|
||||
+ guint32 flags,
|
||||
+ const gchar *text);
|
||||
+static void vnc_connection_handle_clipboard_request(VncConnection *conn);
|
||||
|
||||
/*
|
||||
* A special GSource impl which allows us to wait on a certain
|
||||
@@ -311,6 +315,8 @@ enum {
|
||||
VNC_DISCONNECTED,
|
||||
VNC_ERROR,
|
||||
|
||||
+ VNC_CLIPBOARD_REQUEST, /* 20 */
|
||||
+
|
||||
VNC_LAST_SIGNAL,
|
||||
};
|
||||
|
||||
@@ -5606,6 +5612,15 @@ static void vnc_connection_class_init(Vn
|
||||
G_TYPE_NONE,
|
||||
1,
|
||||
G_TYPE_STRING);
|
||||
+ signals[VNC_CLIPBOARD_REQUEST] =
|
||||
+ g_signal_new ("clipboard-request",
|
||||
+ G_OBJECT_CLASS_TYPE (object_class),
|
||||
+ G_SIGNAL_RUN_FIRST,
|
||||
+ G_STRUCT_OFFSET (VncConnectionClass, vnc_clipboard_request),
|
||||
+ NULL, NULL,
|
||||
+ g_cclosure_marshal_VOID__VOID,
|
||||
+ G_TYPE_NONE,
|
||||
+ 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -6921,7 +6936,7 @@ vnc_connection_handle_extended_clipboard
|
||||
} else if (action == VNC_CLIPBOARD_ACTION_PROVIDE) {
|
||||
VNC_DEBUG("Received clipboard action 'provide', not implemented yet");
|
||||
} else if (action == VNC_CLIPBOARD_ACTION_REQUEST) {
|
||||
- VNC_DEBUG("Received clipboard action 'request', not implemented yet");
|
||||
+ vnc_connection_handle_clipboard_request(conn);
|
||||
} else if (action == VNC_CLIPBOARD_ACTION_PEEK) {
|
||||
VNC_DEBUG("Received clipboard action 'peek', not implemented yet");
|
||||
} else if (action == VNC_CLIPBOARD_ACTION_NOTIFY) {
|
||||
@@ -6987,3 +7002,92 @@ void vnc_connection_flush_pending_clipbo
|
||||
vnc_connection_announce_clipboard(conn, TRUE);
|
||||
priv->pendingClientClipboard = FALSE;
|
||||
}
|
||||
+
|
||||
+static void
|
||||
+vnc_connection_write_clipboard_provide(VncConnection *conn,
|
||||
+ guint32 flags,
|
||||
+ const gchar *text)
|
||||
+{
|
||||
+ guint8 pad[3] = {0};
|
||||
+ z_stream zst;
|
||||
+ GByteArray *uncompressed_payload = NULL;
|
||||
+ guchar *compressed_buf = NULL;
|
||||
+ gsize compressed_size = 0;
|
||||
+ gulong text_len = strlen(text) + 1;
|
||||
+ guint32 text_len_net;
|
||||
+ gulong dest_len;
|
||||
+
|
||||
+ uncompressed_payload = g_byte_array_new();
|
||||
+
|
||||
+ text_len_net = g_htonl(text_len);
|
||||
+ g_byte_array_append(uncompressed_payload,
|
||||
+ (const guint8 *)&text_len_net,
|
||||
+ sizeof(text_len_net));
|
||||
+
|
||||
+ g_byte_array_append(uncompressed_payload, (const guint8 *)text, text_len);
|
||||
+
|
||||
+ dest_len = compressBound(uncompressed_payload->len);
|
||||
+ compressed_buf = g_malloc(dest_len);
|
||||
+
|
||||
+ zst.zalloc = Z_NULL;
|
||||
+ zst.zfree = Z_NULL;
|
||||
+ zst.opaque = Z_NULL;
|
||||
+
|
||||
+ if (deflateInit(&zst, Z_DEFAULT_COMPRESSION) != Z_OK) {
|
||||
+ vnc_connection_set_error(conn,
|
||||
+ "Failed to initialize zlib for compression");
|
||||
+ g_free(compressed_buf);
|
||||
+ g_byte_array_free(uncompressed_payload, TRUE);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ zst.avail_in = uncompressed_payload->len;
|
||||
+ zst.next_in = uncompressed_payload->data;
|
||||
+ zst.avail_out = dest_len;
|
||||
+ zst.next_out = compressed_buf;
|
||||
+
|
||||
+ if (deflate(&zst, Z_FINISH) != Z_STREAM_END) {
|
||||
+ vnc_connection_set_error(conn, "%s", "Zlib compression failed");
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+ compressed_size = zst.total_out;
|
||||
+
|
||||
+ VNC_DEBUG("Sending clipboard provide, payload size %u, compressed size %lu",
|
||||
+ uncompressed_payload->len, (gulong)compressed_size);
|
||||
+
|
||||
+ vnc_connection_buffered_write_u8(conn, VNC_CONNECTION_CLIENT_MESSAGE_CUT_TEXT);
|
||||
+ vnc_connection_buffered_write(conn, pad, 3);
|
||||
+
|
||||
+ vnc_connection_buffered_write_s32(conn, -(4 + compressed_size));
|
||||
+ vnc_connection_buffered_write_u32(conn, flags | VNC_CLIPBOARD_ACTION_PROVIDE);
|
||||
+
|
||||
+ vnc_connection_buffered_write(conn, compressed_buf, compressed_size);
|
||||
+
|
||||
+ vnc_connection_buffered_flush(conn);
|
||||
+
|
||||
+cleanup:
|
||||
+ deflateEnd(&zst);
|
||||
+ g_free(compressed_buf);
|
||||
+ g_byte_array_free(uncompressed_payload, TRUE);
|
||||
+}
|
||||
+
|
||||
+static void vnc_connection_handle_clipboard_request(VncConnection *conn)
|
||||
+{
|
||||
+ g_signal_emit(conn, signals[VNC_CLIPBOARD_REQUEST], 0);
|
||||
+}
|
||||
+
|
||||
+void vnc_connection_send_clipboard_data(VncConnection *conn,
|
||||
+ const gchar *text)
|
||||
+{
|
||||
+ VncConnectionPrivate *priv = conn->priv;
|
||||
+
|
||||
+ if (priv->server_clipboard_flags & VNC_CLIPBOARD_ACTION_PROVIDE) {
|
||||
+ vnc_connection_write_clipboard_provide(conn,
|
||||
+ VNC_CLIPBOARD_FORMAT_UTF8,
|
||||
+ text);
|
||||
+ } else {
|
||||
+ vnc_connection_set_error(conn,
|
||||
+ "Server does not support clipboard 'provide'"
|
||||
+ "action, can not send clipboard data.");
|
||||
+ }
|
||||
+}
|
||||
--- a/src/vncconnection.h
|
||||
+++ b/src/vncconnection.h
|
||||
@@ -83,6 +83,7 @@ struct _VncConnectionClass
|
||||
void (*vnc_power_control_initialized)(VncConnection *conn);
|
||||
void (*vnc_power_control_failed)(VncConnection *conn);
|
||||
void (*vnc_desktop_rename)(VncConnection *conn, const char *name);
|
||||
+ void (*vnc_clipboard_request) (VncConnection *conn);
|
||||
|
||||
/*
|
||||
* If adding fields to this struct, remove corresponding
|
||||
@@ -249,6 +250,7 @@ gboolean vnc_connection_set_framebuffer(
|
||||
void vnc_connection_handle_clipboard_change(VncConnection *conn);
|
||||
void vnc_connection_clear_pending_flag(VncConnection *conn);
|
||||
void vnc_connection_flush_pending_clipboard(VncConnection *conn);
|
||||
+void vnc_connection_send_clipboard_data(VncConnection *conn, const gchar *text);
|
||||
|
||||
const char *vnc_connection_get_name(VncConnection *conn);
|
||||
int vnc_connection_get_width(VncConnection *conn);
|
||||
--- a/src/vncdisplay.c
|
||||
+++ b/src/vncdisplay.c
|
||||
@@ -356,6 +356,11 @@ static GdkCursor *create_default_cursor(
|
||||
return cursor;
|
||||
}
|
||||
|
||||
+static void on_clipboard_request(VncConnection *conn, VncDisplay *display);
|
||||
+static void send_primary_selection(GtkClipboard *clipboard G_GNUC_UNUSED,
|
||||
+ const gchar *text,
|
||||
+ gpointer user_data);
|
||||
+
|
||||
#ifdef G_OS_WIN32
|
||||
static HWND win32_window = NULL;
|
||||
|
||||
@@ -1347,6 +1352,11 @@ static void realize_event(GtkWidget *wid
|
||||
|
||||
gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(obj)),
|
||||
priv->remote_cursor ? priv->remote_cursor : priv->null_cursor);
|
||||
+
|
||||
+ if (priv->conn) {
|
||||
+ g_signal_connect(priv->conn, "clipboard-request",
|
||||
+ G_CALLBACK(on_clipboard_request), obj);
|
||||
+ }
|
||||
}
|
||||
|
||||
static void get_preferred_width(GtkWidget *widget,
|
||||
@@ -1501,6 +1511,53 @@ static void on_framebuffer_update(VncCon
|
||||
}
|
||||
|
||||
|
||||
+/**
|
||||
+ * on_clipboard_request:
|
||||
+ *
|
||||
+ * This function is called when the VncConnection receives a clipboard
|
||||
+ * request from the server. Its job is to start the *asynchronous*
|
||||
+ * process of getting the local PRIMARY selection text from GTK.
|
||||
+ */
|
||||
+static void
|
||||
+on_clipboard_request(VncConnection *conn G_GNUC_UNUSED, VncDisplay *display)
|
||||
+{
|
||||
+ GtkClipboard *primary_clipboard;
|
||||
+
|
||||
+ VNC_DEBUG("Handling 'clipboard-request' signal, asking GTK for PRIMARY selection.");
|
||||
+
|
||||
+ primary_clipboard = gtk_widget_get_clipboard(GTK_WIDGET(display),
|
||||
+ GDK_SELECTION_PRIMARY);
|
||||
+
|
||||
+ /*
|
||||
+ * This is an asynchronous operation. GTK will fetch the text
|
||||
+ * and call send_primary_selection() when it's done.
|
||||
+ */
|
||||
+ gtk_clipboard_request_text(primary_clipboard,
|
||||
+ send_primary_selection,
|
||||
+ display);
|
||||
+}
|
||||
+
|
||||
+/**
|
||||
+ * send_primary_selection:
|
||||
+ *
|
||||
+ * GTK calls this function after it has successfully fetched the PRIMARY
|
||||
+ * selection text. We now have the data and can send it to the server.
|
||||
+ */
|
||||
+static void
|
||||
+send_primary_selection(GtkClipboard *clipboard G_GNUC_UNUSED,
|
||||
+ const gchar *text,
|
||||
+ gpointer user_data)
|
||||
+{
|
||||
+ VncDisplay *display = VNC_DISPLAY(user_data);
|
||||
+ VncConnection *conn = vnc_display_get_connection(display);
|
||||
+
|
||||
+ if (conn && text != NULL && *text != '\0') {
|
||||
+ VNC_DEBUG("Got PRIMARY selection asynchronously, sending to server.");
|
||||
+ vnc_connection_send_clipboard_data(conn, text);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+
|
||||
static void do_framebuffer_init(VncDisplay *obj,
|
||||
const VncPixelFormat *remoteFormat,
|
||||
int width, int height, gboolean quiet)
|
||||
@@ -0,0 +1,96 @@
|
||||
Subject: Implement handling of server clipboard NOTIFY action
|
||||
From: Lin Ma lma@suse.de Mon Jul 7 13:11:06 2025 +0800
|
||||
Date: Sun Oct 12 16:55:56 2025 +0800:
|
||||
Git: b9150485968c771bdb32dc8a95560f6baa3c91eb
|
||||
|
||||
This patch implements the first step in the server-to-client clipboard
|
||||
synchronization flow: the client's response to a server update
|
||||
notification.
|
||||
|
||||
The specific changes are as follows:
|
||||
- When VncConnection receives a message from the server with the
|
||||
'VNC_CLIPBOARD_ACTION_NOTIFY' flag, the new
|
||||
'vnc_connection_handle_clipboard_notify' function is called.
|
||||
- Within this function, the client checks if the server supports the
|
||||
'REQUEST' action. If so, the client immediately sends back a 'REQUEST'
|
||||
message, asking the server to provide its clipboard data.
|
||||
- Additionally, upon receiving a notification from the server, any
|
||||
pending client-to-server clipboard update ('pendingClientClipboard')
|
||||
is cancelled. This ensures correct clipboard state synchronization.
|
||||
|
||||
This commit establishes the "Notify -> Request" interaction, laying the
|
||||
groundwork for the client to receive clipboard data from the server.
|
||||
|
||||
Signed-off-by: Lin Ma <lma@suse.de>
|
||||
|
||||
--- a/src/vncconnection.c
|
||||
+++ b/src/vncconnection.c
|
||||
@@ -155,6 +155,10 @@ static void vnc_connection_write_clipboa
|
||||
guint32 flags,
|
||||
const gchar *text);
|
||||
static void vnc_connection_handle_clipboard_request(VncConnection *conn);
|
||||
+static void vnc_connection_write_clipboard_request(VncConnection *conn,
|
||||
+ guint32 flags);
|
||||
+static void vnc_connection_handle_clipboard_notify(VncConnection *conn,
|
||||
+ guint32 flags);
|
||||
|
||||
/*
|
||||
* A special GSource impl which allows us to wait on a certain
|
||||
@@ -6940,7 +6944,7 @@ vnc_connection_handle_extended_clipboard
|
||||
} else if (action == VNC_CLIPBOARD_ACTION_PEEK) {
|
||||
VNC_DEBUG("Received clipboard action 'peek', not implemented yet");
|
||||
} else if (action == VNC_CLIPBOARD_ACTION_NOTIFY) {
|
||||
- VNC_DEBUG("Received clipboard action 'notify', not implemented yet");
|
||||
+ vnc_connection_handle_clipboard_notify(conn, flags);
|
||||
} else {
|
||||
VNC_DEBUG("Unknown extended clipboard action 0x%x", action);
|
||||
}
|
||||
@@ -7091,3 +7095,48 @@ void vnc_connection_send_clipboard_data(
|
||||
"action, can not send clipboard data.");
|
||||
}
|
||||
}
|
||||
+
|
||||
+static void
|
||||
+vnc_connection_write_clipboard_request(VncConnection *conn, guint32 flags)
|
||||
+{
|
||||
+ VncConnectionPrivate *priv = conn->priv;
|
||||
+ guint8 pad[3] = {0};
|
||||
+
|
||||
+ if (!(priv->server_clipboard_flags & VNC_CLIPBOARD_ACTION_REQUEST)) {
|
||||
+ vnc_connection_set_error(conn,
|
||||
+ "Server does not support clipboard 'request'"
|
||||
+ "action.");
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ VNC_DEBUG("Sending clipboard request to server for format flags 0x%x", flags);
|
||||
+
|
||||
+ vnc_connection_buffered_write_u8(conn, VNC_CONNECTION_CLIENT_MESSAGE_CUT_TEXT);
|
||||
+ vnc_connection_buffered_write(conn, pad, 3);
|
||||
+
|
||||
+ vnc_connection_buffered_write_s32(conn, -4);
|
||||
+ vnc_connection_buffered_write_u32(conn, flags | VNC_CLIPBOARD_ACTION_REQUEST);
|
||||
+
|
||||
+ vnc_connection_buffered_flush(conn);
|
||||
+}
|
||||
+
|
||||
+static void
|
||||
+vnc_connection_handle_clipboard_notify(VncConnection *conn, guint32 flags)
|
||||
+{
|
||||
+ VncConnectionPrivate *priv = conn->priv;
|
||||
+
|
||||
+ if (flags & VNC_CLIPBOARD_FORMAT_UTF8) {
|
||||
+ priv->pendingClientClipboard = FALSE;
|
||||
+
|
||||
+ VNC_DEBUG("Received notification of new clipboard data on server.");
|
||||
+
|
||||
+ if (priv->server_clipboard_flags & VNC_CLIPBOARD_ACTION_REQUEST) {
|
||||
+ vnc_connection_write_clipboard_request(conn, VNC_CLIPBOARD_FORMAT_UTF8);
|
||||
+ } else {
|
||||
+ VNC_DEBUG("Server does not support clipboard request, not requesting data.");
|
||||
+ vnc_connection_set_error(conn,
|
||||
+ "Server does not support clipboard "
|
||||
+ "'notify' action.");
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
293
008-Complete-server-to-client-data-sync-PROVIDE.patch
Normal file
293
008-Complete-server-to-client-data-sync-PROVIDE.patch
Normal file
@@ -0,0 +1,293 @@
|
||||
Subject: Complete server-to-client data sync (PROVIDE)
|
||||
From: Lin Ma lma@suse.de Mon Jul 7 13:14:40 2025 +0800
|
||||
Date: Sun Oct 12 16:55:56 2025 +0800:
|
||||
Git: 6e6d5df86cb5b6fe032573e9fd4defe88f04a733
|
||||
|
||||
This patch implements the final stage of the server-to-client extended
|
||||
clipboard synchronization, enabling the client to receive and process
|
||||
the actual data sent by the server via 'VNC_CLIPBOARD_ACTION_PROVIDE'.
|
||||
|
||||
The main workflow is as follows:
|
||||
1. Upon receiving a 'PROVIDE' message, VncConnection reads and
|
||||
decompresses the data payload using zlib.
|
||||
2. It parses the decompressed data, extracts the text content, and
|
||||
handles any potential Unicode escape sequences.
|
||||
3. After validating the data as legal UTF-8, it emits a new GObject
|
||||
signal, 'vnc-clipboard-data-received', passing the text as an argument.
|
||||
4. VncDisplay listens for this signal and, in its callback, sets the
|
||||
received text to the local 'CLIPBOARD' and 'PRIMARY' selections.
|
||||
|
||||
Signed-off-by: Lin Ma <lma@suse.de>
|
||||
|
||||
--- a/src/vncconnection.c
|
||||
+++ b/src/vncconnection.c
|
||||
@@ -159,6 +159,9 @@ static void vnc_connection_write_clipboa
|
||||
guint32 flags);
|
||||
static void vnc_connection_handle_clipboard_notify(VncConnection *conn,
|
||||
guint32 flags);
|
||||
+static void vnc_connection_handle_clipboard_provide(VncConnection *conn,
|
||||
+ guint32 flags,
|
||||
+ gint32 len);
|
||||
|
||||
/*
|
||||
* A special GSource impl which allows us to wait on a certain
|
||||
@@ -320,6 +323,7 @@ enum {
|
||||
VNC_ERROR,
|
||||
|
||||
VNC_CLIPBOARD_REQUEST, /* 20 */
|
||||
+ VNC_CLIPBOARD_DATA_RECEIVED,
|
||||
|
||||
VNC_LAST_SIGNAL,
|
||||
};
|
||||
@@ -5625,6 +5629,16 @@ static void vnc_connection_class_init(Vn
|
||||
g_cclosure_marshal_VOID__VOID,
|
||||
G_TYPE_NONE,
|
||||
0);
|
||||
+ signals[VNC_CLIPBOARD_DATA_RECEIVED] =
|
||||
+ g_signal_new("vnc-clipboard-data-received",
|
||||
+ G_TYPE_FROM_CLASS(klass),
|
||||
+ G_SIGNAL_RUN_LAST,
|
||||
+ 0,
|
||||
+ NULL, NULL,
|
||||
+ g_cclosure_marshal_VOID__STRING,
|
||||
+ G_TYPE_NONE,
|
||||
+ 1,
|
||||
+ G_TYPE_STRING);
|
||||
}
|
||||
|
||||
|
||||
@@ -6899,12 +6913,6 @@ vnc_connection_handle_extended_clipboard
|
||||
len);
|
||||
return;
|
||||
}
|
||||
- if (len > VNC_MAX_EXTENDED_CLIPBOARD_MSG_SIZE) {
|
||||
- vnc_connection_set_error(conn,
|
||||
- "Extended clipboard message too long (%d bytes).",
|
||||
- len);
|
||||
- return;
|
||||
- }
|
||||
|
||||
flags = vnc_connection_read_u32(conn);
|
||||
if (vnc_connection_has_error(conn))
|
||||
@@ -6938,7 +6946,7 @@ vnc_connection_handle_extended_clipboard
|
||||
vnc_connection_handle_clipboard_caps(conn, flags, lengths);
|
||||
|
||||
} else if (action == VNC_CLIPBOARD_ACTION_PROVIDE) {
|
||||
- VNC_DEBUG("Received clipboard action 'provide', not implemented yet");
|
||||
+ vnc_connection_handle_clipboard_provide(conn, flags, len);
|
||||
} else if (action == VNC_CLIPBOARD_ACTION_REQUEST) {
|
||||
vnc_connection_handle_clipboard_request(conn);
|
||||
} else if (action == VNC_CLIPBOARD_ACTION_PEEK) {
|
||||
@@ -7140,3 +7148,167 @@ vnc_connection_handle_clipboard_notify(V
|
||||
}
|
||||
}
|
||||
}
|
||||
+
|
||||
+/**
|
||||
+ * unescape_unicode_string:
|
||||
+ * @escaped_text: A string that may contain \uXXXX escape sequences.
|
||||
+ *
|
||||
+ * This function parses a string containing C-style Unicode escapes
|
||||
+ * (e.g., "\u4f60\u597d") and converts it back to a proper UTF-8
|
||||
+ * encoded string (e.g., "你好").
|
||||
+ *
|
||||
+ * Returns: A new, g_malloc'd string with the unescaped UTF-8 content.
|
||||
+ * The caller is responsible for freeing the returned string.
|
||||
+ * Returns NULL on failure.
|
||||
+ */
|
||||
+static gchar*
|
||||
+unescape_unicode_string(const gchar *escaped_text)
|
||||
+{
|
||||
+ if (!escaped_text)
|
||||
+ return NULL;
|
||||
+
|
||||
+ GString *result = g_string_new("");
|
||||
+ const gchar *p = escaped_text;
|
||||
+
|
||||
+ while (*p) {
|
||||
+ if (p[0] == '\\' && p[1] == 'u' &&
|
||||
+ g_ascii_isxdigit(p[2]) && g_ascii_isxdigit(p[3]) &&
|
||||
+ g_ascii_isxdigit(p[4]) && g_ascii_isxdigit(p[5]))
|
||||
+ {
|
||||
+ gchar hex[5];
|
||||
+ gunichar uc;
|
||||
+
|
||||
+ strncpy(hex, p + 2, 4);
|
||||
+ hex[4] = '\0';
|
||||
+ uc = g_ascii_strtoull(hex, NULL, 16);
|
||||
+
|
||||
+ g_string_append_unichar(result, uc);
|
||||
+ p += 6;
|
||||
+ } else {
|
||||
+ g_string_append_c(result, *p);
|
||||
+ p++;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return g_string_free(result, FALSE);
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * Handles the clipboard 'provide' action. It reads the compressed
|
||||
+ * data from the connection, decompresses it, parses the content,
|
||||
+ * and emits the "vnc-clipboard-data-received" signal if successful.
|
||||
+ */
|
||||
+static void
|
||||
+vnc_connection_handle_clipboard_provide(VncConnection *conn, guint32 flags, gint32 len)
|
||||
+{
|
||||
+ guint8 *zlib_data = NULL;
|
||||
+ guint8 *uncompressed_data = NULL;
|
||||
+ GByteArray *out_array = NULL;
|
||||
+ gchar *escaped_text = NULL;
|
||||
+ gchar *text = NULL;
|
||||
+ z_stream zst;
|
||||
+ gboolean zst_initialized = FALSE;
|
||||
+
|
||||
+ gsize uncompressed_size = 0;
|
||||
+ guint32 text_len = 0;
|
||||
+
|
||||
+ if (!(flags & VNC_CLIPBOARD_FORMAT_UTF8)) {
|
||||
+ vnc_connection_set_error(conn, "%s", "Unsupported format UTF8.");
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+
|
||||
+ len -= 4;
|
||||
+ // read the entire compressed data block from the network connection.
|
||||
+ zlib_data = g_malloc(len);
|
||||
+ if (!vnc_connection_read(conn, zlib_data, len)) {
|
||||
+ vnc_connection_set_error(conn, "Failed to read clipboard provide data");
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+
|
||||
+ // Decompress the entire block of data.
|
||||
+ zst.zalloc = Z_NULL;
|
||||
+ zst.zfree = Z_NULL;
|
||||
+ zst.opaque = Z_NULL;
|
||||
+ zst.avail_in = len;
|
||||
+ zst.next_in = zlib_data;
|
||||
+
|
||||
+ if (inflateInit(&zst) != Z_OK) {
|
||||
+ vnc_connection_set_error(conn, "%s", "Failed to initialize zlib for decompression");
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+ zst_initialized = TRUE;
|
||||
+
|
||||
+ out_array = g_byte_array_new();
|
||||
+ guint8 temp_buf[4096];
|
||||
+ int ret;
|
||||
+
|
||||
+ do {
|
||||
+ zst.avail_out = sizeof(temp_buf);
|
||||
+ zst.next_out = temp_buf;
|
||||
+ ret = inflate(&zst, Z_NO_FLUSH);
|
||||
+ if (ret != Z_OK && ret != Z_STREAM_END) {
|
||||
+ vnc_connection_set_error(conn,
|
||||
+ "Zlib decompression failed with code %d",
|
||||
+ ret);
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+ g_byte_array_append(out_array, temp_buf, sizeof(temp_buf) - zst.avail_out);
|
||||
+ } while (zst.avail_out == 0 && ret != Z_STREAM_END);
|
||||
+
|
||||
+ uncompressed_data = g_byte_array_free(out_array, FALSE);
|
||||
+ out_array = NULL;
|
||||
+ uncompressed_size = zst.total_out;
|
||||
+ inflateEnd(&zst);
|
||||
+ zst_initialized = FALSE;
|
||||
+
|
||||
+ // parse [length][data] from the decompressed data.
|
||||
+ if (uncompressed_size < 4) {
|
||||
+ vnc_connection_set_error(conn,
|
||||
+ "Invalid decompressed data: too short for "
|
||||
+ "length field");
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+
|
||||
+ text_len = g_ntohl(*(guint32 *)uncompressed_data);
|
||||
+
|
||||
+ if (text_len > uncompressed_size - 4) {
|
||||
+ vnc_connection_set_error(conn, "Invalid decompressed data: length mismatch");
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+
|
||||
+ if (text_len > VNC_MAX_EXTENDED_CLIPBOARD_MSG_SIZE) {
|
||||
+ VNC_DEBUG("Clipboard data size (%u) exceeds maximum size (%u), ignoring.",
|
||||
+ text_len,
|
||||
+ VNC_MAX_EXTENDED_CLIPBOARD_MSG_SIZE);
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+
|
||||
+ escaped_text = g_strndup((const gchar *)(uncompressed_data + 4), text_len);
|
||||
+ text = unescape_unicode_string(escaped_text);
|
||||
+
|
||||
+ if (!g_utf8_validate(text, -1, NULL)) {
|
||||
+ VNC_DEBUG("Invalid UTF-8 sequence in clipboard data");
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+
|
||||
+ VNC_DEBUG("Successfully decoded clipboard text (len=%u), "
|
||||
+ "emitting signal to UI layer.",
|
||||
+ text_len);
|
||||
+ g_signal_emit(conn, signals[VNC_CLIPBOARD_DATA_RECEIVED], 0, text);
|
||||
+
|
||||
+cleanup:
|
||||
+ g_free(zlib_data);
|
||||
+ g_free(uncompressed_data);
|
||||
+ g_free(escaped_text);
|
||||
+ g_free(text);
|
||||
+
|
||||
+ if (out_array) {
|
||||
+ g_byte_array_free(out_array, TRUE);
|
||||
+ }
|
||||
+
|
||||
+ if (zst_initialized) {
|
||||
+ inflateEnd(&zst);
|
||||
+ }
|
||||
+
|
||||
+ return;
|
||||
+}
|
||||
--- a/src/vncdisplay.c
|
||||
+++ b/src/vncdisplay.c
|
||||
@@ -360,6 +360,9 @@ static void on_clipboard_request(VncConn
|
||||
static void send_primary_selection(GtkClipboard *clipboard G_GNUC_UNUSED,
|
||||
const gchar *text,
|
||||
gpointer user_data);
|
||||
+static void on_clipboard_data_received(VncConnection *conn G_GNUC_UNUSED,
|
||||
+ const gchar *text,
|
||||
+ gpointer user_data);
|
||||
|
||||
#ifdef G_OS_WIN32
|
||||
static HWND win32_window = NULL;
|
||||
@@ -1557,6 +1560,23 @@ send_primary_selection(GtkClipboard *cli
|
||||
}
|
||||
}
|
||||
|
||||
+static void
|
||||
+on_clipboard_data_received(VncConnection *conn G_GNUC_UNUSED,
|
||||
+ const gchar *text,
|
||||
+ gpointer user_data)
|
||||
+{
|
||||
+ VncDisplay *display = VNC_DISPLAY(user_data);
|
||||
+ GtkClipboard *clipboard;
|
||||
+
|
||||
+ clipboard = gtk_widget_get_clipboard(GTK_WIDGET(display),
|
||||
+ gdk_atom_intern("CLIPBOARD", FALSE));
|
||||
+ gtk_clipboard_set_text(clipboard, text, -1);
|
||||
+
|
||||
+ clipboard = gtk_widget_get_clipboard(GTK_WIDGET(display),
|
||||
+ GDK_SELECTION_PRIMARY);
|
||||
+ gtk_clipboard_set_text(clipboard, text, -1);
|
||||
+}
|
||||
+
|
||||
|
||||
static void do_framebuffer_init(VncDisplay *obj,
|
||||
const VncPixelFormat *remoteFormat,
|
||||
@@ -3129,6 +3149,8 @@ static void vnc_display_init(VncDisplay
|
||||
G_CALLBACK(on_primary_owner_change), display);
|
||||
g_signal_connect(display, "focus-in-event",
|
||||
G_CALLBACK(on_focus_in), display);
|
||||
+ g_signal_connect(G_OBJECT(priv->conn), "vnc-clipboard-data-received",
|
||||
+ G_CALLBACK(on_clipboard_data_received), display);
|
||||
|
||||
priv->keycode_map = vnc_display_keymap_gdk2rfb_table(&priv->keycode_maplen);
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
-------------------------------------------------------------------
|
||||
Mon Nov 3 13:38:54 MST 2025 - carnold@suse.com
|
||||
|
||||
- bsc#1251850 - removal of spice leads to regression in
|
||||
functionality, specifically for graphical console copy paste
|
||||
001-src-introduce-a-vncclipboard.h-header-file.patch
|
||||
002-Add-the-extended-clipboard-pseudo-encoding.patch
|
||||
003-Implement-extended-clipboard-capability-negotiation.patch
|
||||
004-Implement-client-to-server-clipboard-update-notification.patch
|
||||
005-Flush-pending-clipboard-on-focus-in-event.patch
|
||||
006-Implement-response-to-server-clipboard-REQUEST-action.patch
|
||||
007-Implement-handling-of-server-clipboard-NOTIFY-action.patch
|
||||
008-Complete-server-to-client-data-sync-PROVIDE.patch
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Fri Feb 7 09:58:09 UTC 2025 - Bjørn Lie <bjorn.lie@gmail.com>
|
||||
|
||||
|
||||
11
gtk-vnc.spec
11
gtk-vnc.spec
@@ -29,6 +29,15 @@ Group: Development/Libraries/X11
|
||||
URL: https://wiki.gnome.org/Projects/gtk-vnc
|
||||
Source0: https://download.gnome.org/sources/gtk-vnc/1.5/%{name}-%{version}.tar.xz
|
||||
|
||||
Patch1: 001-src-introduce-a-vncclipboard.h-header-file.patch
|
||||
Patch2: 002-Add-the-extended-clipboard-pseudo-encoding.patch
|
||||
Patch3: 003-Implement-extended-clipboard-capability-negotiation.patch
|
||||
Patch4: 004-Implement-client-to-server-clipboard-update-notification.patch
|
||||
Patch5: 005-Flush-pending-clipboard-on-focus-in-event.patch
|
||||
Patch6: 006-Implement-response-to-server-clipboard-REQUEST-action.patch
|
||||
Patch7: 007-Implement-handling-of-server-clipboard-NOTIFY-action.patch
|
||||
Patch8: 008-Complete-server-to-client-data-sync-PROVIDE.patch
|
||||
|
||||
BuildRequires: cyrus-sasl-devel
|
||||
BuildRequires: gobject-introspection-devel >= 0.9.4
|
||||
BuildRequires: intltool
|
||||
@@ -157,7 +166,7 @@ threaded.
|
||||
%lang_package
|
||||
|
||||
%prep
|
||||
%autosetup
|
||||
%autosetup -p1
|
||||
# install gvncviewer
|
||||
sed -i '/install:/s/false/true/' examples/meson.build
|
||||
|
||||
|
||||
Reference in New Issue
Block a user