SDL: Support X11 incremental clipboard transfers.

From 6bb91c7c085bc6eae490147791d1a2ee37d2cd0f Mon Sep 17 00:00:00 2001
From: Ramez Ragaa <[EMAIL REDACTED]>
Date: Thu, 23 May 2024 17:15:54 +0300
Subject: [PATCH] Support X11 incremental clipboard transfers.

This adds support for pasting large clipboard contents using the INCR
property mechanism

Fixes #8871
---
 src/video/x11/SDL_x11clipboard.c | 126 +++++++++++++++++++++++--------
 src/video/x11/SDL_x11events.c    |  13 ++++
 src/video/x11/SDL_x11video.h     |   1 +
 3 files changed, 110 insertions(+), 30 deletions(-)

diff --git a/src/video/x11/SDL_x11clipboard.c b/src/video/x11/SDL_x11clipboard.c
index 8605285f43e22..a016f2d2cbf32 100644
--- a/src/video/x11/SDL_x11clipboard.c
+++ b/src/video/x11/SDL_x11clipboard.c
@@ -53,6 +53,8 @@ static Window GetWindow(SDL_VideoDevice *_this)
         data->clipboard_window = X11_XCreateWindow(dpy, parent, -10, -10, 1, 1, 0,
                                                    CopyFromParent, InputOnly,
                                                    CopyFromParent, 0, &xattr);
+
+        X11_XSelectInput(dpy, data->clipboard_window, PropertyChangeMask);
         X11_XFlush(data->display);
     }
 
@@ -96,19 +98,64 @@ static int SetSelectionData(SDL_VideoDevice *_this, Atom selection, SDL_Clipboar
     return 0;
 }
 
-static void *CloneDataBuffer(const void *buffer, size_t *len)
+static void *CloneDataBuffer(const void *buffer, const size_t len)
 {
     void *clone = NULL;
-    if (*len > 0 && buffer) {
-        clone = SDL_malloc((*len)+sizeof(Uint32));
+    if (len > 0 && buffer) {
+        clone = SDL_malloc(len + sizeof(Uint32));
         if (clone) {
-            SDL_memcpy(clone, buffer, *len);
-            SDL_memset((Uint8 *)clone + *len, 0, sizeof(Uint32));
+            SDL_memcpy(clone, buffer, len);
+            SDL_memset((Uint8 *)clone + len, 0, sizeof(Uint32));
         }
     }
     return clone;
 }
 
+/*
+ * original_buffer is considered unusable after the function is called.
+ */
+static void *AppendDataBuffer(void *original_buffer, const size_t old_len, const void *buffer, const size_t buffer_len)
+{
+    void *resized_buffer;
+
+    if (buffer_len > 0 && buffer) {
+        resized_buffer = SDL_realloc(original_buffer, old_len + buffer_len + sizeof(Uint32));
+        if (resized_buffer) {
+            SDL_memcpy((Uint8 *)resized_buffer + old_len, buffer, buffer_len);
+            SDL_memset((Uint8 *)resized_buffer + old_len + buffer_len, 0, sizeof(Uint32));
+        }
+
+        return resized_buffer;
+    } else {
+        return original_buffer;
+    }
+}
+
+static SDL_bool WaitForSelection(SDL_VideoDevice *_this, Atom selection_type, SDL_bool *flag)
+{
+    Uint64 waitStart;
+    Uint64 waitElapsed;
+
+    waitStart = SDL_GetTicks();
+    *flag = SDL_TRUE;
+    while (*flag) {
+        SDL_PumpEvents();
+        waitElapsed = SDL_GetTicks() - waitStart;
+        /* Wait one second for a selection response. */
+        if (waitElapsed > 1000) {
+            *flag = SDL_FALSE;
+            SDL_SetError("Selection timeout");
+            /* We need to set the selection text so that next time we won't
+               timeout, otherwise we will hang on every call to this function. */
+            SetSelectionData(_this, selection_type, SDL_ClipboardTextCallback, NULL,
+                             text_mime_types, SDL_arraysize(text_mime_types), 0);
+            return SDL_FALSE;
+        }
+    }
+
+    return SDL_TRUE;
+}
+
 static void *GetSelectionData(SDL_VideoDevice *_this, Atom selection_type,
                               const char *mime_type, size_t *length)
 {
@@ -121,12 +168,11 @@ static void *GetSelectionData(SDL_VideoDevice *_this, Atom selection_type,
     int seln_format;
     unsigned long count;
     unsigned long overflow;
-    Uint64 waitStart;
-    Uint64 waitElapsed;
 
     SDLX11_ClipboardData *clipboard;
     void *data = NULL;
     unsigned char *src = NULL;
+    SDL_bool incr_success = SDL_FALSE;
     Atom XA_MIME = X11_XInternAtom(display, mime_type, False);
     Atom XA_INCR = X11_XInternAtom(display, "INCR", False);
 
@@ -148,7 +194,7 @@ static void *GetSelectionData(SDL_VideoDevice *_this, Atom selection_type,
 
         if (clipboard->callback) {
             const void *clipboard_data = clipboard->callback(clipboard->userdata, mime_type, length);
-            data = CloneDataBuffer(clipboard_data, length);
+            data = CloneDataBuffer(clipboard_data, *length);
         }
     } else {
         /* Request that the selection owner copy the data to our window */
@@ -157,35 +203,55 @@ static void *GetSelectionData(SDL_VideoDevice *_this, Atom selection_type,
         X11_XConvertSelection(display, selection_type, XA_MIME, selection, owner,
                               CurrentTime);
 
-        /* When using synergy on Linux and when data has been put in the clipboard
-           on the remote (Windows anyway) machine then selection_waiting may never
-           be set to False. Time out after a while. */
-        waitStart = SDL_GetTicks();
-        videodata->selection_waiting = SDL_TRUE;
-        while (videodata->selection_waiting) {
-            SDL_PumpEvents();
-            waitElapsed = SDL_GetTicks() - waitStart;
-            /* Wait one second for a selection response. */
-            if (waitElapsed > 1000) {
-                videodata->selection_waiting = SDL_FALSE;
-                SDL_SetError("Selection timeout");
-                /* We need to set the selection text so that next time we won't
-                   timeout, otherwise we will hang on every call to this function. */
-                SetSelectionData(_this, selection_type, SDL_ClipboardTextCallback, NULL,
-                                 text_mime_types, SDL_arraysize(text_mime_types), 0);
-                data = NULL;
-                *length = 0;
-            }
+        if (WaitForSelection(_this, selection_type, &videodata->selection_waiting) == SDL_FALSE) {
+            data = NULL;
+            *length = 0;
         }
 
         if (X11_XGetWindowProperty(display, owner, selection, 0, INT_MAX / 4, False,
                                    XA_MIME, &seln_type, &seln_format, &count, &overflow, &src) == Success) {
             if (seln_type == XA_MIME) {
                 *length = (size_t)count;
-                data = CloneDataBuffer(src, length);
+                data = CloneDataBuffer(src, count);
             } else if (seln_type == XA_INCR) {
-                /* FIXME: Need to implement the X11 INCR protocol */
-                /*SDL_Log("Need to implement the X11 INCR protocol");*/
+                while (1) {
+                    // Only delete the property after being done with the previous "chunk".
+                    X11_XDeleteProperty(display, owner, selection);
+                    X11_XFlush(display);
+
+                    if (WaitForSelection(_this, selection_type, &videodata->selection_incr_waiting) == SDL_FALSE) {
+                        break;
+                    }
+
+                    X11_XFree(src);
+                    if (X11_XGetWindowProperty(display, owner, selection, 0, INT_MAX / 4, False,
+                                           XA_MIME, &seln_type, &seln_format, &count, &overflow, &src) != Success) {
+                        break;
+                    }
+
+                    if (count == 0) {
+                        incr_success = SDL_TRUE;
+                        break;
+                    }
+
+                    if (*length == 0) {
+                        *length = (size_t)count;
+                        data = CloneDataBuffer(src, count);
+                    } else {
+                        data = AppendDataBuffer(data, *length, src, count);
+                        *length += (size_t)count;
+                    }
+
+                    if (data == NULL) {
+                        break;
+                    }
+                }
+
+                if (incr_success == SDL_FALSE) {
+                    SDL_free(data);
+                    data = 0;
+                    *length = 0;
+                }
             }
             X11_XFree(src);
         }
diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c
index ebca6c00263ce..2201c46414a1d 100644
--- a/src/video/x11/SDL_x11events.c
+++ b/src/video/x11/SDL_x11events.c
@@ -764,6 +764,19 @@ static void X11_HandleClipboardEvent(SDL_VideoDevice *_this, const XEvent *xeven
             SDL_zerop(clipboard);
         }
     } break;
+
+    case PropertyNotify:
+    {
+        char *name_of_atom = X11_XGetAtomName(display, xevent->xproperty.atom);
+
+        if (SDL_strncmp(name_of_atom, "SDL_SELECTION", sizeof("SDL_SELECTION") - 1) == 0 && xevent->xproperty.state == PropertyNewValue) {
+            videodata->selection_incr_waiting = SDL_FALSE;
+        }
+
+        if (name_of_atom) {
+            X11_XFree(name_of_atom);
+        }
+    } break;
     }
 }
 
diff --git a/src/video/x11/SDL_x11video.h b/src/video/x11/SDL_x11video.h
index b10b3b30f631a..0df95fa57b99f 100644
--- a/src/video/x11/SDL_x11video.h
+++ b/src/video/x11/SDL_x11video.h
@@ -103,6 +103,7 @@ struct SDL_VideoData
 
     SDL_Scancode key_layout[256];
     SDL_bool selection_waiting;
+    SDL_bool selection_incr_waiting;
 
     SDL_bool broken_pointer_grab; /* true if XGrabPointer seems unreliable. */