SDL: Clipboard data API revamp

From 35876da3c468db13834a80c1baaa0b34dd9d2536 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 3 Jul 2023 23:24:01 -0700
Subject: [PATCH] Clipboard data API revamp

The clipboard data API is now supported on all platforms, at least for internal use.
---
 VisualC-GDK/SDL/SDL.vcxproj                |   3 +-
 VisualC-GDK/SDL/SDL.vcxproj.filters        |   5 +-
 VisualC-WinRT/SDL-UWP.vcxproj              |   1 +
 VisualC-WinRT/SDL-UWP.vcxproj.filters      |   3 +
 VisualC/SDL/SDL.vcxproj                    |   1 +
 VisualC/SDL/SDL.vcxproj.filters            |   3 +
 include/SDL3/SDL_clipboard.h               |  49 +++---
 include/SDL3/SDL_events.h                  |   4 +-
 src/dynapi/SDL_dynapi.sym                  |   2 +-
 src/dynapi/SDL_dynapi_overrides.h          |   2 +-
 src/dynapi/SDL_dynapi_procs.h              |   6 +-
 src/events/SDL_clipboardevents_c.h         |   1 -
 src/events/SDL_events.c                    |   2 +
 src/test/SDL_test_common.c                 | 146 +++++++++++++++--
 src/video/SDL_clipboard.c                  | 180 +++++++++++++++++----
 src/video/SDL_clipboard_c.h                |  29 ++++
 src/video/SDL_sysvideo.h                   |  12 +-
 src/video/SDL_video.c                      |   3 +
 src/video/cocoa/SDL_cocoaclipboard.h       |   6 +-
 src/video/cocoa/SDL_cocoaclipboard.m       |  97 +++++------
 src/video/cocoa/SDL_cocoavideo.m           |   2 +-
 src/video/wayland/SDL_waylandclipboard.c   |  93 ++++-------
 src/video/wayland/SDL_waylandclipboard.h   |   6 +-
 src/video/wayland/SDL_waylanddatamanager.c |  55 +++----
 src/video/wayland/SDL_waylanddatamanager.h |  20 +--
 src/video/wayland/SDL_waylandevents.c      |   4 +-
 src/video/wayland/SDL_waylandvideo.c       |   1 -
 src/video/x11/SDL_x11clipboard.c           |  90 +++++------
 src/video/x11/SDL_x11clipboard.h           |   8 +-
 src/video/x11/SDL_x11events.c              |  19 ++-
 src/video/x11/SDL_x11video.c               |   1 -
 test/testautomation_clipboard.c            |   7 +-
 32 files changed, 549 insertions(+), 312 deletions(-)
 create mode 100644 src/video/SDL_clipboard_c.h

diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj
index 6d898f98eaa9..fd9d84557ae6 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj
+++ b/VisualC-GDK/SDL/SDL.vcxproj
@@ -511,6 +511,7 @@
     <ClInclude Include="..\..\src\video\SDL_blit_auto.h" />
     <ClInclude Include="..\..\src\video\SDL_blit_copy.h" />
     <ClInclude Include="..\..\src\video\SDL_blit_slow.h" />
+    <ClInclude Include="..\..\src\video\SDL_clipboard_c.h" />
     <ClInclude Include="..\..\src\video\SDL_egl_c.h" />
     <ClInclude Include="..\..\src\video\SDL_pixels_c.h" />
     <ClInclude Include="..\..\src\video\SDL_rect_c.h" />
@@ -785,4 +786,4 @@
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
   </ImportGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters
index 769c45215ba8..81dee5cc3d56 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj.filters
+++ b/VisualC-GDK/SDL/SDL.vcxproj.filters
@@ -573,6 +573,9 @@
     <ClInclude Include="..\..\src\video\SDL_blit_slow.h">
       <Filter>video</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\src\video\SDL_clipboard_c.h">
+      <Filter>video</Filter>
+    </ClInclude>
     <ClInclude Include="..\..\src\video\SDL_pixels_c.h">
       <Filter>video</Filter>
     </ClInclude>
@@ -1386,4 +1389,4 @@
   <ItemGroup>
     <ResourceCompile Include="..\..\src\core\windows\version.rc" />
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/VisualC-WinRT/SDL-UWP.vcxproj b/VisualC-WinRT/SDL-UWP.vcxproj
index 3a503634fb13..c6df98236a64 100644
--- a/VisualC-WinRT/SDL-UWP.vcxproj
+++ b/VisualC-WinRT/SDL-UWP.vcxproj
@@ -169,6 +169,7 @@
     <ClInclude Include="..\src\video\SDL_blit_auto.h" />
     <ClInclude Include="..\src\video\SDL_blit_copy.h" />
     <ClInclude Include="..\src\video\SDL_blit_slow.h" />
+    <ClInclude Include="..\src\video\SDL_clipboard_c.h" />
     <ClInclude Include="..\src\video\SDL_egl_c.h" />
     <ClInclude Include="..\src\video\SDL_pixels_c.h" />
     <ClInclude Include="..\src\video\SDL_rect_c.h" />
diff --git a/VisualC-WinRT/SDL-UWP.vcxproj.filters b/VisualC-WinRT/SDL-UWP.vcxproj.filters
index 26bb708b761c..871a43e1ea13 100644
--- a/VisualC-WinRT/SDL-UWP.vcxproj.filters
+++ b/VisualC-WinRT/SDL-UWP.vcxproj.filters
@@ -366,6 +366,9 @@
     <ClInclude Include="..\src\video\SDL_blit_slow.h">
       <Filter>Source Files</Filter>
     </ClInclude>
+    <ClInclude Include="..\src\video\SDL_clipboard_c.h">
+      <Filter>video</Filter>
+    </ClInclude>
     <ClInclude Include="..\src\video\SDL_egl_c.h">
       <Filter>Source Files</Filter>
     </ClInclude>
diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj
index a111aaeb3359..8b8f6d6a79cf 100644
--- a/VisualC/SDL/SDL.vcxproj
+++ b/VisualC/SDL/SDL.vcxproj
@@ -433,6 +433,7 @@
     <ClInclude Include="..\..\src\video\SDL_blit_auto.h" />
     <ClInclude Include="..\..\src\video\SDL_blit_copy.h" />
     <ClInclude Include="..\..\src\video\SDL_blit_slow.h" />
+    <ClInclude Include="..\..\src\video\SDL_clipboard_c.h" />
     <ClInclude Include="..\..\src\video\SDL_egl_c.h" />
     <ClInclude Include="..\..\src\video\SDL_pixels_c.h" />
     <ClInclude Include="..\..\src\video\SDL_rect_c.h" />
diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters
index cb03064bfe91..63fd04c06953 100644
--- a/VisualC/SDL/SDL.vcxproj.filters
+++ b/VisualC/SDL/SDL.vcxproj.filters
@@ -564,6 +564,9 @@
     <ClInclude Include="..\..\src\video\SDL_blit_slow.h">
       <Filter>video</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\src\video\SDL_clipboard_c.h">
+      <Filter>video</Filter>
+    </ClInclude>
     <ClInclude Include="..\..\src\video\SDL_pixels_c.h">
       <Filter>video</Filter>
     </ClInclude>
diff --git a/include/SDL3/SDL_clipboard.h b/include/SDL3/SDL_clipboard.h
index 4e223a57ac9f..ee9d7a821082 100644
--- a/include/SDL3/SDL_clipboard.h
+++ b/include/SDL3/SDL_clipboard.h
@@ -133,9 +133,12 @@ extern DECLSPEC SDL_bool SDLCALL SDL_HasPrimarySelectionText(void);
  * Callback function that will be called when data for the specified mime-type
  * is requested by the OS.
  *
- * \param size      The length of the returned data
- * \param mime_type The requested mime-type
+ * The callback function is called with NULL as the mime_type when the clipboard
+ * is cleared or new data is set. The clipboard is automatically cleared in SDL_Quit().
+ *
  * \param userdata  A pointer to provided user data
+ * \param mime_type The requested mime-type
+ * \param size      A pointer filled in with the length of the returned data
  * \returns a pointer to the data for the provided mime-type. Returning NULL or
  *          setting length to 0 will cause no data to be sent to the "receiver". It is
  *          up to the receiver to handle this. Essentially returning no data is more or
@@ -147,7 +150,18 @@ extern DECLSPEC SDL_bool SDLCALL SDL_HasPrimarySelectionText(void);
  *
  * \sa SDL_SetClipboardData
  */
-typedef void *(SDLCALL *SDL_ClipboardDataCallback)(size_t *size, const char *mime_type, void *userdata);
+typedef const void *(SDLCALL *SDL_ClipboardDataCallback)(void *userdata, const char *mime_type, size_t *size);
+
+/**
+ * Callback function that will be called when the clipboard is cleared, or new data is set.
+ *
+ * \param userdata A pointer to provided user data
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_SetClipboardData
+ */
+typedef void (SDLCALL *SDL_ClipboardCleanupCallback)(void *userdata);
 
 /**
  * Offer clipboard data to the OS
@@ -157,46 +171,39 @@ typedef void *(SDLCALL *SDL_ClipboardDataCallback)(size_t *size, const char *mim
  * data the callback function will be called allowing it to generate and
  * respond with the data for the requested mime-type.
  *
- * The userdata submitted to this function needs to be freed manually. The
- * following scenarios need to be handled:
- *
- * - When the programs clipboard is replaced (cancelled)
- *   SDL_EVENT_CLIPBOARD_CANCELLED
- * - Before calling SDL_Quit()
- *
  * \param callback A function pointer to the function that provides the
  *                 clipboard data
- * \param mime_count The number of mime-types in the mime_types list
+ * \param cleanup A function pointer to the function that cleans up the
+ *                 clipboard data
+ * \param userdata An opaque pointer that will be forwarded to the callbacks
  * \param mime_types A list of mime-types that are being offered
- * \param userdata An opaque pointer that will be forwarded to the callback
+ * \param num_mime_types The number of mime-types in the mime_types list
  * \returns 0 on success or a negative error code on failure; call
  *          SDL_GetError() for more information.
  *
  * \since This function is available since SDL 3.0.0.
  *
  * \sa SDL_ClipboardDataCallback
- * \sa SDL_GetClipboardUserdata
  * \sa SDL_SetClipboardData
  * \sa SDL_GetClipboardData
  * \sa SDL_HasClipboardData
  */
-extern DECLSPEC int SDLCALL SDL_SetClipboardData(SDL_ClipboardDataCallback callback, size_t mime_count,
-                                                 const char **mime_types, void *userdata);
+extern DECLSPEC int SDLCALL SDL_SetClipboardData(SDL_ClipboardDataCallback callback, SDL_ClipboardCleanupCallback cleanup, void *userdata, const char **mime_types, size_t num_mime_types);
 
 /**
- * Retrieve previously set userdata if any.
- *
- * \returns a pointer to the data or NULL if no data exists
+ * Clear the clipboard data
  *
  * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_SetClipboardData
  */
-extern DECLSPEC void *SDLCALL SDL_GetClipboardUserdata(void);
+extern DECLSPEC int SDLCALL SDL_ClearClipboardData();
 
 /**
  * Get the data from clipboard for a given mime type
  *
- * \param length Length of the data
  * \param mime_type The mime type to read from the clipboard
+ * \param size      A pointer filled in with the length of the returned data
  * \returns the retrieved data buffer or NULL on failure; call SDL_GetError()
  *          for more information. Caller must call SDL_free() on the returned
  *          pointer when done with it.
@@ -205,7 +212,7 @@ extern DECLSPEC void *SDLCALL SDL_GetClipboardUserdata(void);
  *
  * \sa SDL_SetClipboardData
  */
-extern DECLSPEC void *SDLCALL SDL_GetClipboardData(size_t *length, const char *mime_type);
+extern DECLSPEC void *SDLCALL SDL_GetClipboardData(const char *mime_type, size_t *size);
 
 /**
  * Query whether there is data in the clipboard for the provided mime type
diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h
index cf80b5d5b784..55ce261daa02 100644
--- a/include/SDL3/SDL_events.h
+++ b/include/SDL3/SDL_events.h
@@ -540,9 +540,9 @@ typedef struct SDL_DropEvent
  *
  * \sa SDL_SetClipboardData
  */
-typedef struct SDL_ClipboardCancelled
+typedef struct SDL_ClipboardEvent
 {
-    Uint32 type;        /**< ::SDL_EVENT_CLIPBOARD_CANCELLED or ::SDL_EVENT_CLIPBOARD_UPDATE */
+    Uint32 type;        /**< ::SDL_EVENT_CLIPBOARD_UPDATE or ::SDL_EVENT_CLIPBOARD_CANCELLED */
     Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
     void *userdata;     /**< User data if any has been set. NULL for ::SDL_EVENT_CLIPBOARD_UPDATE */
 } SDL_ClipboardEvent;
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 2ec47ef1184a..10a0a2f39119 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -162,7 +162,6 @@ SDL3_0.0.0 {
     SDL_GetCPUCount;
     SDL_GetClipboardData;
     SDL_GetClipboardText;
-    SDL_GetClipboardUserdata;
     SDL_GetClosestFullscreenDisplayMode;
     SDL_GetCurrentAudioDriver;
     SDL_GetCurrentDisplayMode;
@@ -869,6 +868,7 @@ SDL3_0.0.0 {
     SDL_wcsncmp;
     SDL_wcsstr;
     SDL_wcstol;
+    SDL_ClearClipboardData;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 16c71f4ec1ee..f4d14b66a186 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -186,7 +186,6 @@
 #define SDL_GetCPUCount SDL_GetCPUCount_REAL
 #define SDL_GetClipboardData SDL_GetClipboardData_REAL
 #define SDL_GetClipboardText SDL_GetClipboardText_REAL
-#define SDL_GetClipboardUserdata SDL_GetClipboardUserdata_REAL
 #define SDL_GetClosestFullscreenDisplayMode SDL_GetClosestFullscreenDisplayMode_REAL
 #define SDL_GetCurrentAudioDriver SDL_GetCurrentAudioDriver_REAL
 #define SDL_GetCurrentDisplayMode SDL_GetCurrentDisplayMode_REAL
@@ -895,3 +894,4 @@
 #define SDL_wcstol SDL_wcstol_REAL
 
 /* New API symbols are added at the end */
+#define SDL_ClearClipboardData SDL_ClearClipboardData_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index d4978ec3ca1c..956accf23d67 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -259,9 +259,8 @@ SDL_DYNAPI_PROC(int,SDL_GetAudioStreamFormat,(SDL_AudioStream *a, SDL_AudioForma
 SDL_DYNAPI_PROC(char*,SDL_GetBasePath,(void),(),return)
 SDL_DYNAPI_PROC(int,SDL_GetCPUCacheLineSize,(void),(),return)
 SDL_DYNAPI_PROC(int,SDL_GetCPUCount,(void),(),return)
-SDL_DYNAPI_PROC(void*,SDL_GetClipboardData,(size_t *a, const char *b),(a,b),return)
+SDL_DYNAPI_PROC(void*,SDL_GetClipboardData,(const char *a, size_t *b),(a,b),return)
 SDL_DYNAPI_PROC(char*,SDL_GetClipboardText,(void),(),return)
-SDL_DYNAPI_PROC(void*,SDL_GetClipboardUserdata,(void),(),return)
 SDL_DYNAPI_PROC(const SDL_DisplayMode*,SDL_GetClosestFullscreenDisplayMode,(SDL_DisplayID a, int b, int c, float d, SDL_bool e),(a,b,c,d,e),return)
 SDL_DYNAPI_PROC(const char*,SDL_GetCurrentAudioDriver,(void),(),return)
 SDL_DYNAPI_PROC(const SDL_DisplayMode*,SDL_GetCurrentDisplayMode,(SDL_DisplayID a),(a),return)
@@ -666,7 +665,7 @@ SDL_DYNAPI_PROC(int,SDL_SendGamepadEffect,(SDL_Gamepad *a, const void *b, int c)
 SDL_DYNAPI_PROC(int,SDL_SendJoystickEffect,(SDL_Joystick *a, const void *b, int c),(a,b,c),return)
 SDL_DYNAPI_PROC(void,SDL_SetAssertionHandler,(SDL_AssertionHandler a, void *b),(a,b),)
 SDL_DYNAPI_PROC(int,SDL_SetAudioStreamFormat,(SDL_AudioStream *a, SDL_AudioFormat b, int c, int d, SDL_AudioFormat e, int f, int g),(a,b,c,d,e,f,g),return)
-SDL_DYNAPI_PROC(int,SDL_SetClipboardData,(SDL_ClipboardDataCallback a, size_t b, const char **c, void *d),(a,b,c,d),return)
+SDL_DYNAPI_PROC(int,SDL_SetClipboardData,(SDL_ClipboardDataCallback a, SDL_ClipboardCleanupCallback b, void *c, const char **d, size_t e),(a,b,c,d,e),return)
 SDL_DYNAPI_PROC(int,SDL_SetClipboardText,(const char *a),(a),return)
 SDL_DYNAPI_PROC(int,SDL_SetCursor,(SDL_Cursor *a),(a),return)
 SDL_DYNAPI_PROC(void,SDL_SetEventEnabled,(Uint32 a, SDL_bool b),(a,b),)
@@ -940,3 +939,4 @@ SDL_DYNAPI_PROC(wchar_t*,SDL_wcsstr,(const wchar_t *a, const wchar_t *b),(a,b),r
 SDL_DYNAPI_PROC(long,SDL_wcstol,(const wchar_t *a, wchar_t **b, int c),(a,b,c),return)
 
 /* New API symbols are added at the end */
+SDL_DYNAPI_PROC(int,SDL_ClearClipboardData,(void),(),return)
diff --git a/src/events/SDL_clipboardevents_c.h b/src/events/SDL_clipboardevents_c.h
index 265eb38935c7..c078042133c3 100644
--- a/src/events/SDL_clipboardevents_c.h
+++ b/src/events/SDL_clipboardevents_c.h
@@ -24,7 +24,6 @@
 #define SDL_clipboardevents_c_h_
 
 extern int SDL_SendClipboardUpdate(void);
-
 extern int SDL_SendClipboardCancelled(void *userdata);
 
 #endif /* SDL_clipboardevents_c_h_ */
diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c
index 865a9b063ae0..b228738f414d 100644
--- a/src/events/SDL_events.c
+++ b/src/events/SDL_events.c
@@ -210,6 +210,8 @@ static void SDL_LogEvent(const SDL_Event *event)
         break;
         SDL_EVENT_CASE(SDL_EVENT_CLIPBOARD_UPDATE)
         break;
+        SDL_EVENT_CASE(SDL_EVENT_CLIPBOARD_CANCELLED)
+        break;
         SDL_EVENT_CASE(SDL_EVENT_RENDER_TARGETS_RESET)
         break;
         SDL_EVENT_CASE(SDL_EVENT_RENDER_DEVICE_RESET)
diff --git a/src/test/SDL_test_common.c b/src/test/SDL_test_common.c
index 0e7595e4a81a..ffb721009f43 100644
--- a/src/test/SDL_test_common.c
+++ b/src/test/SDL_test_common.c
@@ -1757,6 +1757,9 @@ static void SDLTest_PrintEvent(SDL_Event *event)
     case SDL_EVENT_CLIPBOARD_UPDATE:
         SDL_Log("SDL EVENT: Clipboard updated");
         break;
+    case SDL_EVENT_CLIPBOARD_CANCELLED:
+        SDL_Log("SDL EVENT: Clipboard ownership canceled");
+        break;
 
     case SDL_EVENT_FINGER_MOTION:
         SDL_Log("SDL EVENT: Finger: motion touch=%ld, finger=%ld, x=%f, y=%f, dx=%f, dy=%f, pressure=%f",
@@ -1824,10 +1827,69 @@ static void SDLTest_PrintEvent(SDL_Event *event)
     }
 }
 
-static void SDLTest_ScreenShot(SDL_Renderer *renderer)
+#define SCREENSHOT_FILE "screenshot.bmp"
+
+typedef struct
+{
+    void *image;
+    size_t size;
+} SDLTest_ClipboardData;
+
+static void SDLTest_ScreenShotClipboardCleanup(void *context)
+{
+    SDLTest_ClipboardData *data = (SDLTest_ClipboardData *)context;
+
+    SDL_Log("Cleaning up screenshot image data\n");
+
+    if (data->image) {
+        SDL_free(data->image);
+    }
+    SDL_free(data);
+}
+
+static const void *SDLTest_ScreenShotClipboardProvider(void *context, const char *mime_type, size_t *size)
+{
+    SDLTest_ClipboardData *data = (SDLTest_ClipboardData *)context;
+
+    SDL_Log("Providing screenshot image data to clipboard!\n");
+
+    if (!data->image) {
+        SDL_RWops *file;
+
+        file = SDL_RWFromFile(SCREENSHOT_FILE, "r");
+        if (file) {
+            size_t length = (size_t)SDL_RWsize(file);
+            void *image = SDL_malloc(length);
+            if (image) {
+                if (SDL_RWread(file, image, length) != length) {
+                    SDL_Log("Couldn't read %s: %s\n", SCREENSHOT_FILE, SDL_GetError());
+                    SDL_free(image);
+                    image = NULL;
+                }
+            }
+            SDL_RWclose(file);
+
+            if (image) {
+                data->image = image;
+                data->size = length;
+            }
+        } else {
+            SDL_Log("Couldn't load %s: %s\n", SCREENSHOT_FILE, SDL_GetError());
+        }
+    }
+
+    *size = data->size;
+    return data->image;
+}
+
+static void SDLTest_CopyScreenShot(SDL_Renderer *renderer)
 {
     SDL_Rect viewport;
     SDL_Surface *surface;
+    const char *image_formats[] = {
+        "image/bmp"
+    };
+    SDLTest_ClipboardData *clipboard_data;
 
     if (renderer == NULL) {
         return;
@@ -1849,11 +1911,50 @@ static void SDLTest_ScreenShot(SDL_Renderer *renderer)
         return;
     }
 
-    if (SDL_SaveBMP(surface, "screenshot.bmp") < 0) {
-        SDL_Log("Couldn't save screenshot.bmp: %s\n", SDL_GetError());
+    if (SDL_SaveBMP(surface, SCREENSHOT_FILE) < 0) {
+        SDL_Log("Couldn't save %s: %s\n", SCREENSHOT_FILE, SDL_GetError());
         SDL_free(surface);
         return;
     }
+    SDL_free(surface);
+
+    clipboard_data = (SDLTest_ClipboardData *)SDL_calloc(1, sizeof(*clipboard_data));
+    if (!clipboard_data) {
+        SDL_Log("Couldn't allocate clipboard data\n");
+        return;
+    }
+    SDL_SetClipboardData(SDLTest_ScreenShotClipboardProvider, SDLTest_ScreenShotClipboardCleanup, clipboard_data, image_formats, SDL_arraysize(image_formats));
+    SDL_Log("Saved screenshot to %s and clipboard\n", SCREENSHOT_FILE);
+}
+
+static void SDLTest_PasteScreenShot(void)
+{
+    const char *image_formats[] = {
+        "image/bmp",
+        "image/png",
+        "image/tiff",
+    };
+    size_t i;
+
+    for (i = 0; i < SDL_arraysize(image_formats); ++i) {
+        size_t size;
+        void *data = SDL_GetClipboardData(image_formats[i], &size);
+        if (data) {
+            char filename[16];
+            SDL_RWops *file;
+
+            SDL_snprintf(filename, sizeof(filename), "clipboard.%s", image_formats[i] + 6);
+            file = SDL_RWFromFile(filename, "w");
+            if (file) {
+                SDL_Log("Writing clipboard image to %s", filename);
+                SDL_RWwrite(file, data, size);
+                SDL_RWclose(file);
+            }
+            SDL_free(data);
+            return;
+        }
+    }
+    SDL_Log("No supported screenshot data in the clipboard");
 }
 
 static void FullscreenTo(SDLTest_CommonState *state, int index, int windowId)
@@ -1974,7 +2075,7 @@ void SDLTest_CommonEvent(SDLTest_CommonState *state, SDL_Event *event, int *done
             if (window) {
                 for (i = 0; i < state->num_windows; ++i) {
                     if (window == state->windows[i]) {
-                        SDLTest_ScreenShot(state->renderers[i]);
+                        SDLTest_CopyScreenShot(state->renderers[i]);
                     }
                 }
             }
@@ -2080,12 +2181,24 @@ void SDLTest_CommonEvent(SDLTest_CommonState *state, SDL_Event *event, int *done
                 }
             }
             break;
-
         case SDLK_c:
             if (withControl) {
-                /* Ctrl-C copy awesome text! */
-                SDL_SetClipboardText("SDL rocks!\nYou know it!");
-                SDL_Log("Copied text to clipboard\n");
+                if (withShift) {
+                    /* Ctrl-Shift-C copy screenshot! */
+                    SDL_Window *window = SDL_GetWindowFromID(event->key.windowID);
+                    if (window) {
+                        for (i = 0; i < state->num_windows; ++i) {
+                            if (window == state->windows[i]) {
+                                SDLTest_CopyScreenShot(state->renderers[i]);
+                            }
+                        }
+                    }
+                } else {
+                    /* Ctrl-C copy awesome text! */
+                    SDL_SetClipboardText("SDL rocks!\nYou know it!");
+                    SDL_Log("Copied text to clipboard\n");
+                }
+                break;
             }
             if (withAlt) {
                 /* Alt-C toggle a render clip rectangle */
@@ -2118,14 +2231,19 @@ void SDLTest_CommonEvent(SDLTest_CommonState *state, SDL_Event *event, int *done
             break;
         case SDLK_v:
             if (withControl) {
-                /* Ctrl-V paste awesome text! */
-                char *text = SDL_GetClipboardText();
-                if (*text) {
-                    SDL_Log("Clipboard: %s\n", text);
+                if (withShift) {
+                    /* Ctrl-Shift-V paste screenshot! */
+                    SDLTest_PasteScreenShot();
                 } else {
-                    SDL_Log("Clipboard is empty\n");
+                    /* Ctrl-V paste awesome text! */
+                    char *text = SDL_GetClipboardText();
+                    if (*text) {
+                        SDL_Log("Clipboard: %s\n", text);
+                    } else {
+                        SDL_Log("Clipboard is empty\n");
+                    }
+                    SDL_free(text);
                 }
-                SDL_free(text);
             }
             break;
         case SDLK_f:
diff --git a/src/video/SDL_clipboard.c b/src/video/SDL_clipboard.c
index d6809e1b4f4c..8858f4b45531 100644
--- a/src/video/SDL_clipboard.c
+++ b/src/video/SDL_clipboard.c
@@ -20,21 +20,103 @@
 */
 #include "SDL_internal.h"
 
+#include "SDL_clipboard_c.h"
 #include "SDL_sysvideo.h"
+#include "../events/SDL_clipboardevents_c.h"
 
-int SDL_SetClipboardData(SDL_ClipboardDataCallback callback, size_t mime_count, const char **mime_types, void *userdata)
+
+void SDL_CancelClipboardData(Uint32 sequence)
+{
+    SDL_VideoDevice *_this = SDL_GetVideoDevice();
+    size_t i;
+
+    if (sequence != _this->clipboard_sequence) {
+        /* This clipboard data was already canceled */
+        return;
+    }
+
+    SDL_SendClipboardCancelled(_this->clipboard_userdata);
+
+    if (_this->clipboard_cleanup) {
+        _this->clipboard_cleanup(_this->clipboard_userdata);
+    }
+
+    if (_this->clipboard_mime_types) {
+        for (i = 0; i < _this->num_clipboard_mime_types; ++i) {
+            SDL_free(_this->clipboard_mime_types[i]);
+        }
+        SDL_free(_this->clipboard_mime_types);
+        _this->clipboard_mime_types = NULL;
+        _this->num_clipboard_mime_types = 0;
+    }
+
+    _this->clipboard_callback = NULL;
+    _this->clipboard_cleanup = NULL;
+    _this->clipboard_userdata = NULL;
+}
+
+int SDL_SetClipboardData(SDL_ClipboardDataCallback callback, SDL_ClipboardCleanupCallback cleanup, void *userdata, const char **mime_types, size_t num_mime_types)
 {
     SDL_VideoDevice *_this = SDL_GetVideoDevice();
+    size_t i;
 
     if (_this == NULL) {
         return SDL_SetError("Video subsystem must be initialized to set clipboard text");
     }
 
+    /* Parameter validation */
+    if (!((callback && mime_types && num_mime_types > 0) ||
+          (!callback && !mime_types && num_mime_types == 0))) {
+        return SDL_SetError("Invalid parameters");
+    }
+
+    if (!callback && !_this->clipboard_callback) {
+        /* Nothing to do, don't modify the system clipboard */
+        return 0;
+    }
+
+    SDL_CancelClipboardData(_this->clipboard_sequence);
+
+    ++_this->clipboard_sequence;
+    if (!_this->clipboard_sequence) {
+        _this->clipboard_sequence = 1;
+    }
+    _this->clipboard_callback = callback;
+    _this->clipboard_cleanup = cleanup;
+    _this->clipboard_userdata = userdata;
+
+    if (mime_types && num_mime_types > 0) {
+        size_t num_allocated = 0;
+
+        _this->clipboard_mime_types = (char **)SDL_malloc(num_mime_types * sizeof(char *));
+        if (_this->clipboard_mime_types) {
+            for (i = 0; i < num_mime_types; ++i) {
+                _this->clipboard_mime_types[i] = SDL_strdup(mime_types[i]);
+                if (_this->clipboard_mime_types[i]) {
+                    ++num_allocated;
+                }
+            }
+        }
+        if (num_allocated < num_mime_types) {
+            SDL_ClearClipboardData();
+            return SDL_OutOfMemory();
+        }
+        _this->num_clipboard_mime_types = num_mime_types;
+    }
+
     if (_this->SetClipboardData) {
-        return _this->SetClipboardData(_this, callback, mime_count, mime_types, userdata);
-    } else {
-        return SDL_Unsupported();
+        if (_this->SetClipboardData(_this) < 0) {
+            return -1;
+        }
     }
+
+    SDL_SendClipboardUpdate();
+    return 0;
+}
+
+int SDL_ClearClipboardData(void)
+{
+    return SDL_SetClipboardData(NULL, NULL, NULL, NULL, 0);
 }
 
 int SDL_SetClipboardText(const char *text)
@@ -49,12 +131,16 @@ int SDL_SetClipboardText(const char *text)
         text = "";
     }
     if (_this->SetClipboardText) {
-        return _this->SetClipboardText(_this, text);
+        if (_this->SetClipboardText(_this, text) < 0) {
+            return -1;
+        }
     } else {
         SDL_free(_this->clipboard_text);
         _this->clipboard_text = SDL_strdup(text);
-        return 0;
     }
+
+    SDL_SendClipboardUpdate();
+    return 0;
 }
 
 int SDL_SetPrimarySelectionText(const char *text)
@@ -69,27 +155,55 @@ int SDL_SetPrimarySelectionText(const char *text)
         text = "";
     }
     if (_this->SetPrimarySelectionText) {
-        return _this->SetPrimarySelectionText(_this, text);
+        if (_this->SetPrimarySelectionText(_this, text) < 0) {
+            return -1;
+        }
     } else {
         SDL_free(_this->primary_selection_text);
         _this->primary_selection_text = SDL_strdup(text);
-        return 0;
     }
+
+    SDL_SendClipboardUpdate();
+    return 0;
 }
 
-void *SDL_GetClipboardData(size_t *length, const char *mime_type)
+void *SDL_GetClipboardData(const char *mime_type, size_t *size)
 {
     SDL_VideoDevice *_this = SDL_GetVideoDevice();
+    void *data = NULL;
+
     if (_this == NULL) {
         SDL_SetError("Video subsystem must be initialized to get clipboard data");
         return NULL;
     }
 
-    if (_this->GetClipboardData) {
-        return _this->GetClipboardData(_this, length, mime_type);
-    } else {
+    if (!mime_type) {
+        SDL_InvalidParamError("mime_type");
         return NULL;
     }
+    if (!size) {
+        SDL_InvalidParamError("size");
+        return NULL;
+    }
+
+    if (_this->GetClipboardData) {
+        data = _this->GetClipboardData(_this, mime_type, size);
+    } else if (_this->clipboard_callback) {
+        const void *provided_data = _this->clipboard_callback(_this->clipboard_userdata, mime_type, size);
+        if (provided_data) {
+            /* Make a copy of it for the caller */
+            data = SDL_malloc(*size);
+            if (data) {
+                SDL_memcpy(data, provided_data, *size);
+            } else {
+                SDL_OutOfMemory();
+            }
+        }
+    }
+    if (!data) {
+        *size = 0;
+    }
+    return data;
 }
 
 char *SDL_GetClipboardText(void)
@@ -135,15 +249,28 @@ char *SDL_GetPrimarySelectionText(void)
 SDL_bool SDL_HasClipboardData(const char *mime_type)
 {
     SDL_VideoDevice *_this = SDL_GetVideoDevice();
+    size_t i;
+
     if (_this == NULL) {
         SDL_SetError("Video subsystem must be initialized to check clipboard data");
         return SDL_FALSE;
     }
 
+    if (!mime_type) {
+        SDL_InvalidParamError("mime_type");
+        return SDL_FALSE;
+    }
+
     if (_this->HasClipboardData) {
         return _this->HasClipboardData(_this, mime_type);
+    } else {
+        for (i = 0; i < _this->num_clipboard_mime_types; ++i) {
+            if (SDL_strcmp(mime_type, _this->clipboard_mime_types[i]) == 0) {
+                return SDL_TRUE;
+            }
+        }
+        return SDL_FALSE;
     }
-    return SDL_FALSE;
 }
 
 SDL_bool SDL_HasClipboardText(void)
@@ -177,26 +304,11 @@ SDL_bool SDL_HasPrimarySelectionText(void)
 
     if (_this->HasPrimarySelectionText) {
         return _this->HasPrimarySelectionText(_this);
+    } else {
+        if (_this->primary_selection_text && _this->primary_selection_text[0] != '\0') {
+            return SDL_TRUE;
+        } else {
+            return SDL_FALSE;
+        }
     }
-
-    if (_this->primary_selection_text && _this->primary_selection_text[0] != '\0') {
-        return SDL_TRUE;
-    }
-
-    return SDL_FALSE;
-}
-
-void *SDL_GetClipboardUserdata(void)
-{
-    SDL_VideoDevice *_this = SDL_GetVideoDevice();
-
-    if (_this == NULL) {
-        SDL_SetError("Video subsystem must be initialized to check clipboard userdata");
-        return NULL;
-    }
-
-    if (_this->GetClipboardUserdata) {
-        return _this->GetClipboardUserdata(_this);
-    }
-    return NULL;
 }
diff --git a/src/video/SDL_clipboard_c.h b/src/video/SDL_clipboard_c.h
new file mode 100644
index 000000000000..e741f888d170
--- /dev/null
+++ b/src/video/SDL_clipboard_c.h
@@ -0,0 +1,29 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be

(Patch may be truncated, please check the link at the top of this post.)