SDL: x11: Adds support for generic clipboard data in X11

From da3fefc65cff69c9683af70bfddb8e9e59e7fee7 Mon Sep 17 00:00:00 2001
From: Linus Probert <[EMAIL REDACTED]>
Date: Wed, 10 May 2023 10:31:14 +0200
Subject: [PATCH] x11: Adds support for generic clipboard data in X11

Re-writes clipboard data handling in X11 to an on demand approach where
data can be produced on request instead of storing it in X11 properties
on the window.
Primary selection has been changed to mimic this behavior even though
it's only possible to use it for text as of now.
---
 src/video/x11/SDL_x11clipboard.c | 280 +++++++++++++++++++------------
 src/video/x11/SDL_x11clipboard.h |  28 ++--
 src/video/x11/SDL_x11events.c    |  69 +++++---
 src/video/x11/SDL_x11video.c     |   5 +
 src/video/x11/SDL_x11video.h     |   2 +
 5 files changed, 242 insertions(+), 142 deletions(-)

diff --git a/src/video/x11/SDL_x11clipboard.c b/src/video/x11/SDL_x11clipboard.c
index 89549fe2a1c8..5f5a7caa94ea 100644
--- a/src/video/x11/SDL_x11clipboard.c
+++ b/src/video/x11/SDL_x11clipboard.c
@@ -26,6 +26,42 @@
 
 #include "SDL_x11video.h"
 #include "SDL_x11clipboard.h"
+#include "../../events/SDL_events_c.h"
+
+#define TEXT_MIME_TYPES_LEN 5
+static const char *text_mime_types[TEXT_MIME_TYPES_LEN] = {
+    "text/plain;charset=utf-8",
+    "text/plain",
+    "TEXT",
+    "UTF8_STRING",
+    "STRING",
+};
+
+static void *X11_ClipboardTextCallback(size_t *length, const char *mime_type, void *userdata)
+{
+    void *data = NULL;
+    SDL_bool valid_mime_type = SDL_FALSE;
+    *length = 0;
+
+    if (userdata == NULL) {
+        return data;
+    }
+
+    for (size_t i = 0; i < TEXT_MIME_TYPES_LEN; ++i) {
+        if (SDL_strcmp(mime_type, text_mime_types[i]) == 0) {
+            valid_mime_type = SDL_TRUE;
+            break;
+        }
+    }
+
+    if (valid_mime_type) {
+        char *text = userdata;
+        *length = SDL_strlen(text);
+        data = userdata;
+    }
+
+    return data;
+}
 
 /* Get any application owned window handle for clipboard association */
 static Window GetWindow(SDL_VideoDevice *_this)
@@ -49,130 +85,113 @@ static Window GetWindow(SDL_VideoDevice *_this)
     return data->clipboard_window;
 }
 
-/* We use our own cut-buffer for intermediate storage instead of
-   XA_CUT_BUFFER0 because their use isn't really defined for holding UTF8. */
-Atom X11_GetSDLCutBufferClipboardType(Display *display, enum ESDLX11ClipboardMimeType mime_type,
-                                      Atom selection_type)
+static int SetSelectionData(SDL_VideoDevice *_this, Atom selection, SDL_ClipboardDataCallback callback,
+                            size_t mime_count, const char **mime_types, void *userdata, SDL_bool internal)
 {
-    switch (mime_type) {
-    case SDL_X11_CLIPBOARD_MIME_TYPE_STRING:
-    case SDL_X11_CLIPBOARD_MIME_TYPE_TEXT_PLAIN:
-#ifdef X_HAVE_UTF8_STRING
-    case SDL_X11_CLIPBOARD_MIME_TYPE_TEXT_PLAIN_UTF8:
-#endif
-    case SDL_X11_CLIPBOARD_MIME_TYPE_TEXT:
-        return X11_XInternAtom(display, selection_type == XA_PRIMARY ? "SDL_CUTBUFFER_PRIMARY_SELECTION" : "SDL_CUTBUFFER",
-                               False);
-    default:
-        SDL_SetError("Can't find mime_type.");
-        return XA_STRING;
-    }
-}
-
-Atom X11_GetSDLCutBufferClipboardExternalFormat(Display *display, enum ESDLX11ClipboardMimeType mime_type)
-{
-    switch (mime_type) {
-    case SDL_X11_CLIPBOARD_MIME_TYPE_STRING:
-/* If you don't support UTF-8, you might use XA_STRING here */
-#ifdef X_HAVE_UTF8_STRING
-        return X11_XInternAtom(display, "UTF8_STRING", False);
-#else
-        return XA_STRING;
-#endif
-    case SDL_X11_CLIPBOARD_MIME_TYPE_TEXT_PLAIN:
-        return X11_XInternAtom(display, "text/plain", False);
-#ifdef X_HAVE_UTF8_STRING
-    case SDL_X11_CLIPBOARD_MIME_TYPE_TEXT_PLAIN_UTF8:
-        return X11_XInternAtom(display, "text/plain;charset=utf-8", False);
-#endif
-    case SDL_X11_CLIPBOARD_MIME_TYPE_TEXT:
-        return X11_XInternAtom(display, "TEXT", False);
-    default:
-        SDL_SetError("Can't find mime_type.");
-        return XA_STRING;
-    }
-}
-Atom X11_GetSDLCutBufferClipboardInternalFormat(Display *display, enum ESDLX11ClipboardMimeType mime_type)
-{
-    switch (mime_type) {
-    case SDL_X11_CLIPBOARD_MIME_TYPE_STRING:
-    case SDL_X11_CLIPBOARD_MIME_TYPE_TEXT_PLAIN:
-#ifdef X_HAVE_UTF8_STRING
-    case SDL_X11_CLIPBOARD_MIME_TYPE_TEXT_PLAIN_UTF8:
-#endif
-    case SDL_X11_CLIPBOARD_MIME_TYPE_TEXT:
-/* If you don't support UTF-8, you might use XA_STRING here */
-#ifdef X_HAVE_UTF8_STRING
-        return X11_XInternAtom(display, "UTF8_STRING", False);
-#else
-        return XA_STRING;
-#endif
-    default:
-        SDL_SetError("Can't find mime_type.");
-        return XA_STRING;
-    }
-}
-
-static int SetSelectionText(SDL_VideoDevice *_this, const char *text, Atom selection_type)
-{
-    Display *display = _this->driverdata->display;
+    SDL_VideoData *videodata = _this->driverdata;
+    Display *display = videodata->display;
     Window window;
+    SDLX11_ClipboardData *clipboard;
+    SDL_bool clipboard_owner = SDL_FALSE;
 
-    /* Get the SDL window that will own the selection */
     window = GetWindow(_this);
     if (window == None) {
         return SDL_SetError("Couldn't find a window to own the selection");
     }
 
-    /* Save the selection on the root window */
-    X11_XChangeProperty(display, DefaultRootWindow(display),
-                        X11_GetSDLCutBufferClipboardType(display, SDL_X11_CLIPBOARD_MIME_TYPE_STRING, selection_type),
-                        X11_GetSDLCutBufferClipboardInternalFormat(display, SDL_X11_CLIPBOARD_MIME_TYPE_STRING), 8, PropModeReplace,
-                        (const unsigned char *)text, SDL_strlen(text));
+    if (selection == XA_PRIMARY) {
+        clipboard = &videodata->primary_selection;
+    } else {
+        clipboard = &videodata->clipboard;
+    }
 
-    if (X11_XGetSelectionOwner(display, selection_type) != window) {
-        X11_XSetSelectionOwner(display, selection_type, window, CurrentTime);
+    clipboard_owner = X11_XGetSelectionOwner(display, selection) == window;
+
+    /* If we are cancelling our own data we need to clean it up */
+    if (clipboard_owner) {
+        if (clipboard->internal == SDL_TRUE) {
+            SDL_free(clipboard->userdata);
+        } else {
+            SDL_SendClipboardCancelled(clipboard->userdata);
+        }
     }
 
+    clipboard->callback = callback;
+    clipboard->userdata = userdata;
+    clipboard->mime_types = mime_types;
+    clipboard->mime_count = mime_count;
+    clipboard->internal = internal;
+
+    if (!clipboard_owner) {
+        X11_XSetSelectionOwner(display, selection, window, CurrentTime);
+    }
     return 0;
 }
 
-static char *GetSelectionText(SDL_VideoDevice *_this, Atom selection_type)
+static void *CloneDataBuffer(void *buffer, size_t *len, SDL_bool nullterminate)
+{
+    void *clone = NULL;
+    if (*len > 0 && buffer != NULL) {
+        if (nullterminate == SDL_TRUE) {
+            clone = SDL_malloc((*len)+1);
+            if (clone == NULL) {
+                SDL_OutOfMemory();
+            } else {
+                SDL_memcpy(clone, buffer, *len);
+                ((char *) clone)[*len] = '\0';
+                *len += 1;
+            }
+        } else {
+            clone = SDL_malloc(*len);
+            if (clone == NULL) {
+                SDL_OutOfMemory();
+            } else {
+                SDL_memcpy(clone, buffer, *len);
+            }
+        }
+    }
+    return clone;
+}
+
+static void *GetSelectionData(SDL_VideoDevice *_this, Atom selection_type, size_t *length,
+                              const char *mime_type, SDL_bool nullterminate)
 {
     SDL_VideoData *videodata = _this->driverdata;
     Display *display = videodata->display;
-    Atom format;
     Window window;
     Window owner;
     Atom selection;
     Atom seln_type;
     int seln_format;
-    unsigned long nbytes;
     unsigned long overflow;
-    unsigned char *src;
-    char *text;
     Uint64 waitStart;
     Uint64 waitElapsed;
 
-    text = NULL;
+    void *data = NULL;
+    unsigned char *src = NULL;
+    Atom XA_MIME = X11_XInternAtom(display, mime_type, False);
+    *length = 0;
 
     /* Get the window that holds the selection */
     window = GetWindow(_this);
-    format = X11_GetSDLCutBufferClipboardInternalFormat(display, SDL_X11_CLIPBOARD_MIME_TYPE_STRING);
     owner = X11_XGetSelectionOwner(display, selection_type);
     if (owner == None) {
-        /* Fall back to ancient X10 cut-buffers which do not support UTF8 strings*/
-        owner = DefaultRootWindow(display);
-        selection = XA_CUT_BUFFER0;
-        format = XA_STRING;
+        /* This requires a fallback to ancient X10 cut-buffers. We will just skip those for now */
+        return NULL;
     } else if (owner == window) {
         owner = DefaultRootWindow(display);
-        selection = X11_GetSDLCutBufferClipboardType(display, SDL_X11_CLIPBOARD_MIME_TYPE_STRING, selection_type);
+        if (selection_type == XA_PRIMARY) {
+            src = videodata->primary_selection.callback(length, mime_type, videodata->primary_selection.userdata);
+        } else {
+            src = videodata->clipboard.callback(length, mime_type, videodata->clipboard.userdata);
+        }
+
+        data = CloneDataBuffer(src, length, nullterminate);
     } else {
         /* Request that the selection owner copy the data to our window */
         owner = window;
         selection = X11_XInternAtom(display, "SDL_SELECTION", False);
-        X11_XConvertSelection(display, selection_type, format, selection, owner,
+        X11_XConvertSelection(display, selection_type, XA_MIME, selection, owner,
                               CurrentTime);
 
         /* When using synergy on Linux and when data has been put in the clipboard
@@ -189,29 +208,63 @@ static char *GetSelectionText(SDL_VideoDevice *_this, Atom selection_type)
                 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. */
-                SetSelectionText(_this, "", selection_type);
-                return SDL_strdup("");
+                SetSelectionData(_this, selection_type, X11_ClipboardTextCallback, TEXT_MIME_TYPES_LEN,
+                                 text_mime_types, NULL, SDL_TRUE);
+                data = NULL;
+                *length = 0;
             }
         }
-    }
 
-    if (X11_XGetWindowProperty(display, owner, selection, 0, INT_MAX / 4, False,
-                               format, &seln_type, &seln_format, &nbytes, &overflow, &src) == Success) {
-        if (seln_type == format) {
-            text = (char *)SDL_malloc(nbytes + 1);
-            if (text) {
-                SDL_memcpy(text, src, nbytes);
-                text[nbytes] = '\0';
+        if (X11_XGetWindowProperty(display, owner, selection, 0, INT_MAX / 4, False,
+                                   XA_MIME, &seln_type, &seln_format, length, &overflow, &src) == Success) {
+            if (seln_type == XA_MIME) {
+                data = CloneDataBuffer(src, length, nullterminate);
             }
+            X11_XFree(src);
         }
-        X11_XFree(src);
     }
 
-    if (text == NULL) {
-        text = SDL_strdup("");
+    return data;
+}
+
+int X11_SetClipboardData(SDL_VideoDevice *_this, SDL_ClipboardDataCallback callback, size_t mime_count,
+                         const char **mime_types, void *userdata)
+{
+    SDL_VideoData *videodata = _this->driverdata;
+    Atom XA_CLIPBOARD = X11_XInternAtom(videodata->display, "CLIPBOARD", 0);
+    if (XA_CLIPBOARD == None) {
+        return SDL_SetError("Couldn't access X clipboard");
     }
+    return SetSelectionData(_this, XA_CLIPBOARD, callback, mime_count, mime_types, userdata, SDL_FALSE);
+}
 
-    return text;
+void *X11_GetClipboardData(SDL_VideoDevice *_this, size_t *length, const char *mime_type)
+{
+    SDL_VideoData *videodata = _this->driverdata;
+    Atom XA_CLIPBOARD = X11_XInternAtom(videodata->display, "CLIPBOARD", 0);
+    if (XA_CLIPBOARD == None) {
+        SDL_SetError("Couldn't access X clipboard");
+        *length = 0;
+        return NULL;
+    }
+    return GetSelectionData(_this, XA_CLIPBOARD, length, mime_type, SDL_FALSE);
+}
+
+SDL_bool X11_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type)
+{
+    size_t length;
+    void *data;
+    data = X11_GetClipboardData(_this, &length, mime_type);
+    if (data != NULL && length > 0) {
+        SDL_free(data);
+    }
+    return length > 0;
+}
+
+void *X11_GetClipboardUserdata(SDL_VideoDevice *_this)
+{
+    SDLX11_ClipboardData *cb = &_this->driverdata->clipboard;
+    return cb->internal ? NULL : cb->userdata;
 }
 
 int X11_SetClipboardText(SDL_VideoDevice *_this, const char *text)
@@ -221,30 +274,35 @@ int X11_SetClipboardText(SDL_VideoDevice *_this, const char *text)
     if (XA_CLIPBOARD == None) {
         return SDL_SetError("Couldn't access X clipboard");
     }
-    return SetSelectionText(_this, text, XA_CLIPBOARD);
+    return SetSelectionData(_this, XA_CLIPBOARD, X11_ClipboardTextCallback, TEXT_MIME_TYPES_LEN, text_mime_types,
+                            SDL_strdup(text), SDL_TRUE);
 }
 
 int X11_SetPrimarySelectionText(SDL_VideoDevice *_this, const char *text)
 {
-    return SetSelectionText(_this, text, XA_PRIMARY);
+    return SetSelectionData(_this, XA_PRIMARY, X11_ClipboardTextCallback, TEXT_MIME_TYPES_LEN, text_mime_types,
+                            SDL_strdup(text), SDL_TRUE);
 }
 
 char *
 X11_GetClipboardText(SDL_VideoDevice *_this)
 {
+    size_t length;
     SDL_VideoData *videodata = _this->driverdata;
     Atom XA_CLIPBOARD = X11_XInternAtom(videodata->display, "CLIPBOARD", 0);
     if (XA_CLIPBOARD == None) {
         SDL_SetError("Couldn't access X clipboard");
         return SDL_strdup("");
     }
-    return GetSelectionText(_this, XA_CLIPBOARD);
+
+    return GetSelectionData(_this, XA_CLIPBOARD, &length, text_mime_types[0], SDL_TRUE);
 }
 
 char *
 X11_GetPrimarySelectionText(SDL_VideoDevice *_this)
 {
-    return GetSelectionText(_this, XA_PRIMARY);
+    size_t length;
+    return GetSelectionData(_this, XA_PRIMARY, &length, text_mime_types[0], SDL_TRUE);
 }
 
 SDL_bool
@@ -271,4 +329,16 @@ X11_HasPrimarySelectionText(SDL_VideoDevice *_this)
     return result;
 }
 
+void
+X11_QuitClipboard(SDL_VideoDevice *_this)
+{
+    SDL_VideoData *data = _this->driverdata;
+    if (data->primary_selection.internal == SDL_TRUE) {
+        SDL_free(data->primary_selection.userdata);
+    }
+    if (data->clipboard.internal == SDL_TRUE) {
+        SDL_free(data->clipboard.userdata);
+    }
+}
+
 #endif /* SDL_VIDEO_DRIVER_X11 */
diff --git a/src/video/x11/SDL_x11clipboard.h b/src/video/x11/SDL_x11clipboard.h
index 24116ae649f6..1143bc8c32bb 100644
--- a/src/video/x11/SDL_x11clipboard.h
+++ b/src/video/x11/SDL_x11clipboard.h
@@ -23,25 +23,27 @@
 #ifndef SDL_x11clipboard_h_
 #define SDL_x11clipboard_h_
 
-enum ESDLX11ClipboardMimeType
-{
-    SDL_X11_CLIPBOARD_MIME_TYPE_STRING,
-    SDL_X11_CLIPBOARD_MIME_TYPE_TEXT_PLAIN,
-#ifdef X_HAVE_UTF8_STRING
-    SDL_X11_CLIPBOARD_MIME_TYPE_TEXT_PLAIN_UTF8,
-#endif
-    SDL_X11_CLIPBOARD_MIME_TYPE_TEXT,
-    SDL_X11_CLIPBOARD_MIME_TYPE_MAX
-};
+#include <X11/Xlib.h>
 
+typedef struct X11_ClipboardData {
+    SDL_ClipboardDataCallback callback;
+    void *userdata;
+    const char **mime_types;
+    size_t mime_count;
+    SDL_bool internal;
+} SDLX11_ClipboardData;
+
+extern int X11_SetClipboardData(SDL_VideoDevice *_this, SDL_ClipboardDataCallback callback, size_t mime_count,
+                                const char **mime_types, void *userdata);
+extern void *X11_GetClipboardData(SDL_VideoDevice *_this, size_t *length, const char *mime_type);
+extern SDL_bool X11_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type);
+extern void *X11_GetClipboardUserdata(SDL_VideoDevice *_this);
 extern int X11_SetClipboardText(SDL_VideoDevice *_this, const char *text);
 extern char *X11_GetClipboardText(SDL_VideoDevice *_this);
 extern SDL_bool X11_HasClipboardText(SDL_VideoDevice *_this);
 extern int X11_SetPrimarySelectionText(SDL_VideoDevice *_this, const char *text);
 extern char *X11_GetPrimarySelectionText(SDL_VideoDevice *_this);
 extern SDL_bool X11_HasPrimarySelectionText(SDL_VideoDevice *_this);
-extern Atom X11_GetSDLCutBufferClipboardType(Display *display, enum ESDLX11ClipboardMimeType mime_type, Atom selection_type);
-extern Atom X11_GetSDLCutBufferClipboardExternalFormat(Display *display, enum ESDLX11ClipboardMimeType mime_type);
-extern Atom X11_GetSDLCutBufferClipboardInternalFormat(Display *display, enum ESDLX11ClipboardMimeType mime_type);
+extern void X11_QuitClipboard(SDL_VideoDevice *_this);
 
 #endif /* SDL_x11clipboard_h_ */
diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c
index 8c2304971a9f..df058f67d714 100644
--- a/src/video/x11/SDL_x11events.c
+++ b/src/video/x11/SDL_x11events.c
@@ -627,18 +627,28 @@ static void X11_HandleClipboardEvent(SDL_VideoDevice *_this, const XEvent *xeven
     {
         const XSelectionRequestEvent *req = &xevent->xselectionrequest;
         XEvent sevent;
-        int seln_format, mime_formats;
+        int mime_formats;
         unsigned long nbytes;
-        unsigned long overflow;
         unsigned char *seln_data;
-        Atom supportedFormats[SDL_X11_CLIPBOARD_MIME_TYPE_MAX + 1];
         Atom XA_TARGETS = X11_XInternAtom(display, "TARGETS", 0);
+        SDLX11_ClipboardData *clipboard;
 
 #ifdef DEBUG_XEVENTS
-        printf("window CLIPBOARD: SelectionRequest (requestor = %ld, target = %ld)\n",
-               req->requestor, req->target);
+        char *atom_name;
+        atom_name = X11_XGetAtomName(display, req->target);
+        printf("window CLIPBOARD: SelectionRequest (requestor = %ld, target = %ld, mime_type = %s)\n",
+               req->requestor, req->target, atom_name);
+        if (atom_name) {
+            X11_XFree(atom_name);
+        }
 #endif
 
+        if (req->selection == XA_PRIMARY) {
+            clipboard = &videodata->primary_selection;
+        } else {
+            clipboard = &videodata->clipboard;
+        }
+
         SDL_zero(sevent);
         sevent.xany.type = SelectionNotify;
         sevent.xselection.selection = req->selection;
@@ -652,10 +662,12 @@ static void X11_HandleClipboardEvent(SDL_VideoDevice *_this, const XEvent *xeven
            this now (or ever, really). */
 
         if (req->target == XA_TARGETS) {
+            Atom *supportedFormats;
+            supportedFormats = SDL_malloc((clipboard->mime_count + 1) * sizeof(Atom));
             supportedFormats[0] = XA_TARGETS;
             mime_formats = 1;
-            for (i = 0; i < SDL_X11_CLIPBOARD_MIME_TYPE_MAX; ++i) {
-                supportedFormats[mime_formats++] = X11_GetSDLCutBufferClipboardExternalFormat(display, i);
+            for (i = 0; i < clipboard->mime_count; ++i) {
+                supportedFormats[mime_formats++] = X11_XInternAtom(display, clipboard->mime_types[i], False);
             }
             X11_XChangeProperty(display, req->requestor, req->property,
                                 XA_ATOM, 32, PropModeReplace,
@@ -663,25 +675,25 @@ static void X11_HandleClipboardEvent(SDL_VideoDevice *_this, const XEvent *xeven
                                 mime_formats);
             sevent.xselection.property = req->property;
             sevent.xselection.target = XA_TARGETS;
+            SDL_free(supportedFormats);
         } else {
-            for (i = 0; i < SDL_X11_CLIPBOARD_MIME_TYPE_MAX; ++i) {
-                if (X11_GetSDLCutBufferClipboardExternalFormat(display, i) != req->target) {
-                    continue;
-                }
-                if (X11_XGetWindowProperty(display, DefaultRootWindow(display),
-                                           X11_GetSDLCutBufferClipboardType(display, i, req->selection), 0, INT_MAX / 4, False, X11_GetSDLCutBufferClipboardInternalFormat(display, i),
-                                           &sevent.xselection.target, &seln_format, &nbytes,
-                                           &overflow, &seln_data) == Success) {
-                    if (seln_format != None) {
+            if (clipboard->callback) {
+                for (i = 0; i < clipboard->mime_count; ++i) {
+                    const char *mime_type = clipboard->mime_types[i];
+                    if (X11_XInternAtom(display, mime_type, False) != req->target) {
+                        continue;
+                    }
+
+                    /* FIXME: We don't support the X11 INCR protocol for large clipboards. Do we want that? */
+                    seln_data = clipboard->callback(&nbytes, mime_type, clipboard->userdata);
+                    if (seln_data != NULL) {
                         X11_XChangeProperty(display, req->requestor, req->property,
-                                            sevent.xselection.target, seln_format, PropModeReplace,
+                                            req->target, 8, PropModeReplace,
                                             seln_data, nbytes);
                         sevent.xselection.property = req->property;
-                        X11_XFree(seln_data);
-                        break;
-                    } else {
-                        X11_XFree(seln_data);
+                        sevent.xselection.target = req->target;
                     }
+                    break;
                 }
             }
         }
@@ -702,15 +714,24 @@ static void X11_HandleClipboardEvent(SDL_VideoDevice *_this, const XEvent *xeven
     {
         /* !!! FIXME: cache atoms */
         Atom XA_CLIPBOARD = X11_XInternAtom(display, "CLIPBOARD", 0);
+        SDLX11_ClipboardData *clipboard = NULL;
 
 #ifdef DEBUG_XEVENTS
         printf("window CLIPBOARD: SelectionClear (requestor = %ld, target = %ld)\n",
                xevent->xselection.requestor, xevent->xselection.target);
 #endif
 
-        if (xevent->xselectionclear.selection == XA_PRIMARY ||
-            (XA_CLIPBOARD != None && xevent->xselectionclear.selection == XA_CLIPBOARD)) {
-            SDL_SendClipboardUpdate();
+        if (xevent->xselectionclear.selection == XA_PRIMARY) {
+            clipboard = &videodata->primary_selection;
+        } else if (XA_CLIPBOARD != None && xevent->xselectionclear.selection == XA_CLIPBOARD) {
+            clipboard = &videodata->clipboard;
+            if (clipboard->internal == SDL_FALSE) {
+                SDL_SendClipboardCancelled(clipboard->userdata);
+            }
+        }
+        if (clipboard != NULL && clipboard->internal == SDL_TRUE) {
+            SDL_free(clipboard->userdata);
+            clipboard->userdata = NULL;
         }
     } break;
     }
diff --git a/src/video/x11/SDL_x11video.c b/src/video/x11/SDL_x11video.c
index 7d26a685cdcc..6e8ef29fd4ce 100644
--- a/src/video/x11/SDL_x11video.c
+++ b/src/video/x11/SDL_x11video.c
@@ -293,9 +293,13 @@ static SDL_VideoDevice *X11_CreateDevice(void)
 #endif
 #endif
 
+    device->SetClipboardData = X11_SetClipboardData;
+    device->GetClipboardData = X11_GetClipboardData;
+    device->HasClipboardData = X11_HasClipboardData;
     device->SetClipboardText = X11_SetClipboardText;
     device->GetClipboardText = X11_GetClipboardText;
     device->HasClipboardText = X11_HasClipboardText;
+    device->GetClipboardUserdata = X11_GetClipboardUserdata;
     device->SetPrimarySelectionText = X11_SetPrimarySelectionText;
     device->GetPrimarySelectionText = X11_GetPrimarySelectionText;
     device->HasPrimarySelectionText = X11_HasPrimarySelectionText;
@@ -498,6 +502,7 @@ void X11_VideoQuit(SDL_VideoDevice *_this)
     X11_QuitKeyboard(_this);
     X11_QuitMouse(_this);
     X11_QuitTouch(_this);
+    X11_QuitClipboard(_this);
 }
 
 SDL_bool
diff --git a/src/video/x11/SDL_x11video.h b/src/video/x11/SDL_x11video.h
index aca07e14b8ee..0b5df50c7dc8 100644
--- a/src/video/x11/SDL_x11video.h
+++ b/src/video/x11/SDL_x11video.h
@@ -77,6 +77,8 @@ struct SDL_VideoData
     int windowlistlength;
     XID window_group;
     Window clipboard_window;
+    SDLX11_ClipboardData clipboard;
+    SDLX11_ClipboardData primary_selection;
 #ifdef SDL_VIDEO_DRIVER_X11_XFIXES
     SDL_Window *active_cursor_confined_window;
 #endif /* SDL_VIDEO_DRIVER_X11_XFIXES */