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 | |
32 | static 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 |
41 | Window 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 | |
64 | static 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 | |
101 | static 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 | */ |
117 | static 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 | |
134 | static 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 | |
159 | static 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 | |
261 | const 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 | |
267 | bool 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 | |
273 | void *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 | |
279 | bool 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 | |
290 | bool 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 | |
295 | char *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 | |
305 | bool 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 | |
318 | void 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 | |