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.
This commit is contained in:
Linus Probert 2023-05-10 10:31:14 +02:00 committed by Sam Lantinga
parent b48b1ce500
commit da3fefc65c
5 changed files with 245 additions and 145 deletions

View File

@ -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 (X11_XGetSelectionOwner(display, selection_type) != window) {
X11_XSetSelectionOwner(display, selection_type, window, CurrentTime);
if (selection == XA_PRIMARY) {
clipboard = &videodata->primary_selection;
} else {
clipboard = &videodata->clipboard;
}
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,
XA_MIME, &seln_type, &seln_format, length, &overflow, &src) == Success) {
if (seln_type == XA_MIME) {
data = CloneDataBuffer(src, length, nullterminate);
}
X11_XFree(src);
}
}
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';
}
}
X11_XFree(src);
}
return data;
}
if (text == NULL) {
text = SDL_strdup("");
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 */

View File

@ -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_ */

View File

@ -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;
}

View File

@ -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

View File

@ -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 */