SDL: Synchronize clipboard mime types with external clipboard updates

From 6575b8157bec3de9db3c4098568eea6134c5598a Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 31 Dec 2024 13:02:31 -0800
Subject: [PATCH] Synchronize clipboard mime types with external clipboard
 updates

Fixes https://github.com/libsdl-org/SDL/issues/8338
Fixes https://github.com/libsdl-org/SDL/issues/9587
---
 src/events/SDL_clipboardevents.c     |   6 ++
 src/video/SDL_clipboard.c            |  54 ++++++++------
 src/video/SDL_clipboard_c.h          |   5 +-
 src/video/SDL_video.c                |   2 +-
 src/video/cocoa/SDL_cocoaclipboard.m | 105 +++++++++++++++++++++++++--
 5 files changed, 140 insertions(+), 32 deletions(-)

diff --git a/src/events/SDL_clipboardevents.c b/src/events/SDL_clipboardevents.c
index 568d5244fccaf..a852c2cf9b765 100644
--- a/src/events/SDL_clipboardevents.c
+++ b/src/events/SDL_clipboardevents.c
@@ -28,6 +28,12 @@
 
 void SDL_SendClipboardUpdate(bool owner, char **mime_types, size_t num_mime_types)
 {
+    if (!owner) {
+        // Clear our internal clipboard contents when external clipboard is set
+        SDL_CancelClipboardData(0);
+        SDL_SaveClipboardMimeTypes((const char **)mime_types, num_mime_types);
+    }
+
     if (SDL_EventEnabled(SDL_EVENT_CLIPBOARD_UPDATE)) {
         SDL_Event event;
         event.type = SDL_EVENT_CLIPBOARD_UPDATE;
diff --git a/src/video/SDL_clipboard.c b/src/video/SDL_clipboard.c
index a0a65c666c302..51550f44bf3bb 100644
--- a/src/video/SDL_clipboard.c
+++ b/src/video/SDL_clipboard.c
@@ -42,7 +42,7 @@ void SDL_CancelClipboardData(Uint32 sequence)
 {
     SDL_VideoDevice *_this = SDL_GetVideoDevice();
 
-    if (sequence != _this->clipboard_sequence) {
+    if (sequence && sequence != _this->clipboard_sequence) {
         // This clipboard data was already canceled
         return;
     }
@@ -58,10 +58,36 @@ void SDL_CancelClipboardData(Uint32 sequence)
     _this->clipboard_userdata = NULL;
 }
 
+bool SDL_SaveClipboardMimeTypes(const char **mime_types, size_t num_mime_types)
+{
+    SDL_VideoDevice *_this = SDL_GetVideoDevice();
+
+    SDL_FreeClipboardMimeTypes(_this);
+
+    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 (size_t 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_FreeClipboardMimeTypes(_this);
+            return false;
+        }
+        _this->num_clipboard_mime_types = num_mime_types;
+    }
+    return true;
+}
+
 bool 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) {
         return SDL_UninitializedVideo();
@@ -78,7 +104,7 @@ bool SDL_SetClipboardData(SDL_ClipboardDataCallback callback, SDL_ClipboardClean
         return true;
     }
 
-    SDL_CancelClipboardData(_this->clipboard_sequence);
+    SDL_CancelClipboardData(0);
 
     ++_this->clipboard_sequence;
     if (!_this->clipboard_sequence) {
@@ -88,23 +114,9 @@ bool SDL_SetClipboardData(SDL_ClipboardDataCallback callback, SDL_ClipboardClean
     _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 false;
-        }
-        _this->num_clipboard_mime_types = num_mime_types;
+    if (!SDL_SaveClipboardMimeTypes(mime_types, num_mime_types)) {
+        SDL_ClearClipboardData();
+        return false;
     }
 
     if (_this->SetClipboardData) {
@@ -115,7 +127,7 @@ bool SDL_SetClipboardData(SDL_ClipboardDataCallback callback, SDL_ClipboardClean
         char *text = NULL;
         size_t size;
 
-        for (i = 0; i < num_mime_types; ++i) {
+        for (size_t i = 0; i < num_mime_types; ++i) {
             const char *mime_type = _this->clipboard_mime_types[i];
             if (SDL_IsTextMimeType(mime_type)) {
                 const void *data = _this->clipboard_callback(_this->clipboard_userdata, mime_type, &size);
diff --git a/src/video/SDL_clipboard_c.h b/src/video/SDL_clipboard_c.h
index 7cc670b968a2f..64a82fc68dc35 100644
--- a/src/video/SDL_clipboard_c.h
+++ b/src/video/SDL_clipboard_c.h
@@ -39,7 +39,8 @@ extern bool SDL_HasInternalClipboardData(SDL_VideoDevice *_this, const char *mim
 // General purpose clipboard text callback
 const void * SDLCALL SDL_ClipboardTextCallback(void *userdata, const char *mime_type, size_t *size);
 
-void SDLCALL SDL_FreeClipboardMimeTypes(SDL_VideoDevice *_this);
-char ** SDLCALL SDL_CopyClipboardMimeTypes(const char **clipboard_mime_types, size_t num_mime_types, bool temporary);
+bool SDL_SaveClipboardMimeTypes(const char **mime_types, size_t num_mime_types);
+void SDL_FreeClipboardMimeTypes(SDL_VideoDevice *_this);
+char **SDL_CopyClipboardMimeTypes(const char **clipboard_mime_types, size_t num_mime_types, bool temporary);
 
 #endif // SDL_clipboard_c_h_
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index 57fdf689f92f8..73c7dbbe191fc 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -4264,7 +4264,7 @@ void SDL_VideoQuit(void)
     SDL_free(_this->displays);
     _this->displays = NULL;
 
-    SDL_CancelClipboardData(_this->clipboard_sequence);
+    SDL_CancelClipboardData(0);
 
     if (_this->primary_selection_text) {
         SDL_free(_this->primary_selection_text);
diff --git a/src/video/cocoa/SDL_cocoaclipboard.m b/src/video/cocoa/SDL_cocoaclipboard.m
index 9b266fcb5e80c..2de4aa14e0767 100644
--- a/src/video/cocoa/SDL_cocoaclipboard.m
+++ b/src/video/cocoa/SDL_cocoaclipboard.m
@@ -23,8 +23,11 @@
 #ifdef SDL_VIDEO_DRIVER_COCOA
 
 #include "SDL_cocoavideo.h"
+#include "../../events/SDL_events_c.h"
 #include "../../events/SDL_clipboardevents_c.h"
 
+#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
+
 #if MAC_OS_X_VERSION_MAX_ALLOWED < 101300
 typedef NSString *NSPasteboardType; // Defined in macOS 10.13+
 #endif
@@ -72,6 +75,66 @@ - (void)pasteboard:(NSPasteboard *)pasteboard
 
 @end
 
+static char **GetMimeTypes(int *pnformats)
+{
+    char **new_mime_types = NULL;
+
+    *pnformats = 0;
+
+    int nformats = 0;
+    int formatsSz = 0;
+    NSArray<NSPasteboardItem *> *items = [[NSPasteboard generalPasteboard] pasteboardItems];
+    NSUInteger nitems = [items count];
+    if (nitems > 0) {
+        for (NSPasteboardItem *item in items) {
+            NSArray<NSString *> *types = [item types];
+            for (NSString *type in types) {
+                if (@available(macOS 11.0, *)) {
+                    UTType *uttype = [UTType typeWithIdentifier:type];
+                    NSString *mime_type = [uttype preferredMIMEType];
+                    if (mime_type) {
+                        NSUInteger len = [mime_type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
+                        formatsSz += len;
+                        ++nformats;
+                    }
+                }
+                NSUInteger len = [type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
+                formatsSz += len;
+                ++nformats;
+            }
+        }
+
+        new_mime_types = SDL_AllocateTemporaryMemory((nformats + 1) * sizeof(char *) + formatsSz);
+        if (new_mime_types) {
+            int i = 0;
+            char *strPtr = (char *)(new_mime_types + nformats + 1);
+            for (NSPasteboardItem *item in items) {
+                NSArray<NSString *> *types = [item types];
+                for (NSString *type in types) {
+                    if (@available(macOS 11.0, *)) {
+                        UTType *uttype = [UTType typeWithIdentifier:type];
+                        NSString *mime_type = [uttype preferredMIMEType];
+                        if (mime_type) {
+                            NSUInteger len = [mime_type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
+                            SDL_memcpy(strPtr, [mime_type UTF8String], len);
+                            new_mime_types[i++] = strPtr;
+                            strPtr += len;
+                        }
+                    }
+                    NSUInteger len = [type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
+                    SDL_memcpy(strPtr, [type UTF8String], len);
+                    new_mime_types[i++] = strPtr;
+                    strPtr += len;
+                }
+            }
+
+            new_mime_types[nformats] = NULL;
+            *pnformats = nformats;
+        }
+    }
+    return new_mime_types;
+}
+
 
 void Cocoa_CheckClipboardUpdate(SDL_CocoaVideoData *data)
 {
@@ -83,8 +146,11 @@ void Cocoa_CheckClipboardUpdate(SDL_CocoaVideoData *data)
         count = [pasteboard changeCount];
         if (count != data.clipboard_count) {
             if (data.clipboard_count) {
-                // TODO: compute mime types
-                SDL_SendClipboardUpdate(false, NULL, 0);
+                int nformats = 0;
+                char **new_mime_types = GetMimeTypes(&nformats);
+                if (new_mime_types) {
+                    SDL_SendClipboardUpdate(false, new_mime_types, nformats);
+                }
             }
             data.clipboard_count = count;
         }
@@ -129,6 +195,33 @@ bool Cocoa_SetClipboardData(SDL_VideoDevice *_this)
     return true;
 }
 
+static bool IsMimeType(const char *tag)
+{
+    if (SDL_strchr(tag, '/')) {
+        // MIME types have slashes
+        return true;
+    } else if (SDL_strchr(tag, '.')) {
+        // UTI identifiers have periods
+        return false;
+    } else {
+        // Not sure, but it's not a UTI identifier
+        return true;
+    }
+}
+
+static CFStringRef GetUTIType(const char *tag)
+{
+    CFStringRef utiType;
+    if (IsMimeType(tag)) {
+        CFStringRef mimeType = CFStringCreateWithCString(NULL, tag, kCFStringEncodingUTF8);
+        utiType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL);
+        CFRelease(mimeType);
+    } else {
+        utiType = CFStringCreateWithCString(NULL, tag, kCFStringEncodingUTF8);
+    }
+    return utiType;
+}
+
 void *Cocoa_GetClipboardData(SDL_VideoDevice *_this, const char *mime_type, size_t *size)
 {
     @autoreleasepool {
@@ -137,9 +230,7 @@ bool Cocoa_SetClipboardData(SDL_VideoDevice *_this)
         *size = 0;
         for (NSPasteboardItem *item in [pasteboard pasteboardItems]) {
             NSData *itemData;
-            CFStringRef mimeType = CFStringCreateWithCString(NULL, mime_type, kCFStringEncodingUTF8);
-            CFStringRef utiType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL);
-            CFRelease(mimeType);
+            CFStringRef utiType = GetUTIType(mime_type);
             itemData = [item dataForType: (__bridge NSString *)utiType];
             CFRelease(utiType);
             if (itemData != nil) {
@@ -162,9 +253,7 @@ bool Cocoa_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type)
     bool result = false;
     @autoreleasepool {
         NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
-        CFStringRef mimeType = CFStringCreateWithCString(NULL, mime_type, kCFStringEncodingUTF8);
-        CFStringRef utiType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL);
-        CFRelease(mimeType);
+        CFStringRef utiType = GetUTIType(mime_type);
         if ([pasteboard canReadItemWithDataConformingToTypes: @[(__bridge NSString *)utiType]]) {
             result = true;
         }