1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22
23#ifdef SDL_VIDEO_DRIVER_X11
24
25#include <limits.h> // For INT_MAX
26
27#include "SDL_x11video.h"
28#include "SDL_x11clipboard.h"
29#include "../SDL_clipboard_c.h"
30#include "../../events/SDL_events_c.h"
31
32static const char *text_mime_types[] = {
33 "UTF8_STRING",
34 "text/plain;charset=utf-8",
35 "text/plain",
36 "TEXT",
37 "STRING"
38};
39
40// Get any application owned window handle for clipboard association
41Window GetWindow(SDL_VideoDevice *_this)
42{
43 SDL_VideoData *data = _this->internal;
44
45 /* We create an unmapped window that exists just to manage the clipboard,
46 since X11 selection data is tied to a specific window and dies with it.
47 We create the window on demand, so apps that don't use the clipboard
48 don't have to keep an unnecessary resource around. */
49 if (data->clipboard_window == None) {
50 Display *dpy = data->display;
51 Window parent = RootWindow(dpy, DefaultScreen(dpy));
52 XSetWindowAttributes xattr;
53 data->clipboard_window = X11_XCreateWindow(dpy, parent, -10, -10, 1, 1, 0,
54 CopyFromParent, InputOnly,
55 CopyFromParent, 0, &xattr);
56
57 X11_XSelectInput(dpy, data->clipboard_window, PropertyChangeMask);
58 X11_XFlush(data->display);
59 }
60
61 return data->clipboard_window;
62}
63
64static bool SetSelectionData(SDL_VideoDevice *_this, Atom selection, SDL_ClipboardDataCallback callback,
65 void *userdata, const char **mime_types, size_t mime_count, Uint32 sequence)
66{
67 SDL_VideoData *videodata = _this->internal;
68 Display *display = videodata->display;
69 Window window;
70 SDLX11_ClipboardData *clipboard;
71 bool clipboard_owner = false;
72
73 window = GetWindow(_this);
74 if (window == None) {
75 return SDL_SetError("Couldn't find a window to own the selection");
76 }
77
78 if (selection == XA_PRIMARY) {
79 clipboard = &videodata->primary_selection;
80 } else {
81 clipboard = &videodata->clipboard;
82 }
83
84 clipboard_owner = X11_XGetSelectionOwner(display, selection) == window;
85
86 // If we are canceling our own data we need to clean it up
87 if (clipboard_owner && clipboard->sequence == 0) {
88 SDL_free(clipboard->userdata);
89 }
90
91 clipboard->callback = callback;
92 clipboard->userdata = userdata;
93 clipboard->mime_types = mime_types;
94 clipboard->mime_count = mime_count;
95 clipboard->sequence = sequence;
96
97 X11_XSetSelectionOwner(display, selection, window, CurrentTime);
98 return true;
99}
100
101static void *CloneDataBuffer(const void *buffer, const size_t len)
102{
103 void *clone = NULL;
104 if (len > 0 && buffer) {
105 clone = SDL_malloc(len + sizeof(Uint32));
106 if (clone) {
107 SDL_memcpy(clone, buffer, len);
108 SDL_memset((Uint8 *)clone + len, 0, sizeof(Uint32));
109 }
110 }
111 return clone;
112}
113
114/*
115 * original_buffer is considered unusable after the function is called.
116 */
117static void *AppendDataBuffer(void *original_buffer, const size_t old_len, const void *buffer, const size_t buffer_len)
118{
119 void *resized_buffer;
120
121 if (buffer_len > 0 && buffer) {
122 resized_buffer = SDL_realloc(original_buffer, old_len + buffer_len + sizeof(Uint32));
123 if (resized_buffer) {
124 SDL_memcpy((Uint8 *)resized_buffer + old_len, buffer, buffer_len);
125 SDL_memset((Uint8 *)resized_buffer + old_len + buffer_len, 0, sizeof(Uint32));
126 }
127
128 return resized_buffer;
129 } else {
130 return original_buffer;
131 }
132}
133
134static bool WaitForSelection(SDL_VideoDevice *_this, Atom selection_type, bool *flag)
135{
136 Uint64 waitStart;
137 Uint64 waitElapsed;
138
139 waitStart = SDL_GetTicks();
140 *flag = true;
141 while (*flag) {
142 SDL_PumpEvents();
143 waitElapsed = SDL_GetTicks() - waitStart;
144 // Wait one second for a selection response.
145 if (waitElapsed > 1000) {
146 *flag = false;
147 SDL_SetError("Selection timeout");
148 /* We need to set the selection text so that next time we won't
149 timeout, otherwise we will hang on every call to this function. */
150 SetSelectionData(_this, selection_type, SDL_ClipboardTextCallback, NULL,
151 text_mime_types, SDL_arraysize(text_mime_types), 0);
152 return false;
153 }
154 }
155
156 return true;
157}
158
159static void *GetSelectionData(SDL_VideoDevice *_this, Atom selection_type,
160 const char *mime_type, size_t *length)
161{
162 SDL_VideoData *videodata = _this->internal;
163 Display *display = videodata->display;
164 Window window;
165 Window owner;
166 Atom selection;
167 Atom seln_type;
168 int seln_format;
169 unsigned long count;
170 unsigned long overflow;
171
172 SDLX11_ClipboardData *clipboard;
173 void *data = NULL;
174 unsigned char *src = NULL;
175 bool incr_success = false;
176 Atom XA_MIME = X11_XInternAtom(display, mime_type, False);
177
178 *length = 0;
179
180 // Get the window that holds the selection
181 window = GetWindow(_this);
182 owner = X11_XGetSelectionOwner(display, selection_type);
183 if (owner == None) {
184 // This requires a fallback to ancient X10 cut-buffers. We will just skip those for now
185 data = NULL;
186 } else if (owner == window) {
187 owner = DefaultRootWindow(display);
188 if (selection_type == XA_PRIMARY) {
189 clipboard = &videodata->primary_selection;
190 } else {
191 clipboard = &videodata->clipboard;
192 }
193
194 if (clipboard->callback) {
195 const void *clipboard_data = clipboard->callback(clipboard->userdata, mime_type, length);
196 data = CloneDataBuffer(clipboard_data, *length);
197 }
198 } else {
199 // Request that the selection owner copy the data to our window
200 owner = window;
201 selection = videodata->atoms.SDL_SELECTION;
202 X11_XConvertSelection(display, selection_type, XA_MIME, selection, owner,
203 CurrentTime);
204
205 if (WaitForSelection(_this, selection_type, &videodata->selection_waiting) == false) {
206 data = NULL;
207 *length = 0;
208 }
209
210 if (X11_XGetWindowProperty(display, owner, selection, 0, INT_MAX / 4, False,
211 XA_MIME, &seln_type, &seln_format, &count, &overflow, &src) == Success) {
212 if (seln_type == XA_MIME) {
213 *length = (size_t)count;
214 data = CloneDataBuffer(src, count);
215 } else if (seln_type == videodata->atoms.INCR) {
216 while (1) {
217 // Only delete the property after being done with the previous "chunk".
218 X11_XDeleteProperty(display, owner, selection);
219 X11_XFlush(display);
220
221 if (WaitForSelection(_this, selection_type, &videodata->selection_incr_waiting) == false) {
222 break;
223 }
224
225 X11_XFree(src);
226 if (X11_XGetWindowProperty(display, owner, selection, 0, INT_MAX / 4, False,
227 XA_MIME, &seln_type, &seln_format, &count, &overflow, &src) != Success) {
228 break;
229 }
230
231 if (count == 0) {
232 incr_success = true;
233 break;
234 }
235
236 if (*length == 0) {
237 *length = (size_t)count;
238 data = CloneDataBuffer(src, count);
239 } else {
240 data = AppendDataBuffer(data, *length, src, count);
241 *length += (size_t)count;
242 }
243
244 if (data == NULL) {
245 break;
246 }
247 }
248
249 if (incr_success == false) {
250 SDL_free(data);
251 data = 0;
252 *length = 0;
253 }
254 }
255 X11_XFree(src);
256 }
257 }
258 return data;
259}
260
261const char **X11_GetTextMimeTypes(SDL_VideoDevice *_this, size_t *num_mime_types)
262{
263 *num_mime_types = SDL_arraysize(text_mime_types);
264 return text_mime_types;
265}
266
267bool X11_SetClipboardData(SDL_VideoDevice *_this)
268{
269 SDL_VideoData *videodata = _this->internal;
270 return SetSelectionData(_this, videodata->atoms.CLIPBOARD, _this->clipboard_callback, _this->clipboard_userdata, (const char **)_this->clipboard_mime_types, _this->num_clipboard_mime_types, _this->clipboard_sequence);
271}
272
273void *X11_GetClipboardData(SDL_VideoDevice *_this, const char *mime_type, size_t *length)
274{
275 SDL_VideoData *videodata = _this->internal;
276 return GetSelectionData(_this, videodata->atoms.CLIPBOARD, mime_type, length);
277}
278
279bool X11_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type)
280{
281 size_t length;
282 void *data;
283 data = X11_GetClipboardData(_this, mime_type, &length);
284 if (data) {
285 SDL_free(data);
286 }
287 return length > 0;
288}
289
290bool X11_SetPrimarySelectionText(SDL_VideoDevice *_this, const char *text)
291{
292 return SetSelectionData(_this, XA_PRIMARY, SDL_ClipboardTextCallback, SDL_strdup(text), text_mime_types, SDL_arraysize(text_mime_types), 0);
293}
294
295char *X11_GetPrimarySelectionText(SDL_VideoDevice *_this)
296{
297 size_t length;
298 char *text = GetSelectionData(_this, XA_PRIMARY, text_mime_types[0], &length);
299 if (!text) {
300 text = SDL_strdup("");
301 }
302 return text;
303}
304
305bool X11_HasPrimarySelectionText(SDL_VideoDevice *_this)
306{
307 bool result = false;
308 char *text = X11_GetPrimarySelectionText(_this);
309 if (text) {
310 if (text[0] != '\0') {
311 result = true;
312 }
313 SDL_free(text);
314 }
315 return result;
316}
317
318void X11_QuitClipboard(SDL_VideoDevice *_this)
319{
320 SDL_VideoData *data = _this->internal;
321 if (data->primary_selection.sequence == 0) {
322 SDL_free(data->primary_selection.userdata);
323 }
324 if (data->clipboard.sequence == 0) {
325 SDL_free(data->clipboard.userdata);
326 }
327}
328
329#endif // SDL_VIDEO_DRIVER_X11
330