Implement the new clipboard API for Wayland

This commit is contained in:
Kovid Goyal 2022-09-08 17:29:18 +05:30
parent c58d217d32
commit 91c00fb5ac
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 190 additions and 144 deletions

6
glfw/wl_init.c vendored
View File

@ -926,6 +926,8 @@ void _glfwPlatformTerminate(void)
zwp_idle_inhibit_manager_v1_destroy(_glfw.wl.idleInhibitManager); zwp_idle_inhibit_manager_v1_destroy(_glfw.wl.idleInhibitManager);
if (_glfw.wl.dataSourceForClipboard) if (_glfw.wl.dataSourceForClipboard)
wl_data_source_destroy(_glfw.wl.dataSourceForClipboard); wl_data_source_destroy(_glfw.wl.dataSourceForClipboard);
if (_glfw.wl.dataSourceForPrimarySelection)
zwp_primary_selection_source_v1_destroy(_glfw.wl.dataSourceForPrimarySelection);
for (size_t doi=0; doi < arraysz(_glfw.wl.dataOffers); doi++) { for (size_t doi=0; doi < arraysz(_glfw.wl.dataOffers); doi++) {
if (_glfw.wl.dataOffers[doi].id) { if (_glfw.wl.dataOffers[doi].id) {
destroy_data_offer(&_glfw.wl.dataOffers[doi]); destroy_data_offer(&_glfw.wl.dataOffers[doi]);
@ -947,10 +949,6 @@ void _glfwPlatformTerminate(void)
wl_display_disconnect(_glfw.wl.display); wl_display_disconnect(_glfw.wl.display);
} }
finalizePollData(&_glfw.wl.eventLoopData); finalizePollData(&_glfw.wl.eventLoopData);
free(_glfw.wl.clipboardString); _glfw.wl.clipboardString = NULL;
free(_glfw.wl.primarySelectionString); _glfw.wl.primarySelectionString = NULL;
free(_glfw.wl.pasteString); _glfw.wl.pasteString = NULL;
} }
const char* _glfwPlatformGetVersionString(void) const char* _glfwPlatformGetVersionString(void)

3
glfw/wl_platform.h vendored
View File

@ -317,11 +317,8 @@ typedef struct _GLFWlibraryWayland
} egl; } egl;
EventLoopData eventLoopData; EventLoopData eventLoopData;
char* pasteString;
char* clipboardString;
size_t dataOffersCounter; size_t dataOffersCounter;
_GLFWWaylandDataOffer dataOffers[8]; _GLFWWaylandDataOffer dataOffers[8];
char* primarySelectionString;
} _GLFWlibraryWayland; } _GLFWlibraryWayland;
// Wayland-specific per-monitor data // Wayland-specific per-monitor data

277
glfw/wl_window.c vendored
View File

@ -1447,53 +1447,68 @@ void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor)
} }
} }
static void send_text(char *text, int fd) static bool
{ write_all(int fd, const char *data, size_t sz) {
if (text) {
size_t len = strlen(text), pos = 0;
monotonic_t start = glfwGetTime(); monotonic_t start = glfwGetTime();
while (pos < len && glfwGetTime() - start < s_to_monotonic_t(2ll)) { size_t pos = 0;
ssize_t ret = write(fd, text + pos, len - pos); while (pos < sz && glfwGetTime() - start < s_to_monotonic_t(2ll)) {
ssize_t ret = write(fd, data + pos, sz - pos);
if (ret < 0) { if (ret < 0) {
if (errno == EAGAIN || errno == EINTR) continue; if (errno == EAGAIN || errno == EINTR) continue;
_glfwInputError(GLFW_PLATFORM_ERROR, _glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Could not copy writing to destination fd failed with error: %s", strerror(errno)); "Wayland: Could not copy writing to destination fd failed with error: %s", strerror(errno));
break; return false;
} }
if (ret > 0) { if (ret > 0) {
start = glfwGetTime(); start = glfwGetTime();
pos += ret; pos += ret;
} }
} }
return pos >= sz;
}
static void
send_clipboard_data(const _GLFWClipboardData *cd, const char *mime, int fd) {
if (strcmp(mime, "text/plain;charset=utf-8") == 0 || strcmp(mime, "UTF8_STRING") == 0 || strcmp(mime, "TEXT") == 0 || strcmp(mime, "STRING") == 0) mime = "text/plain";
GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype);
void *iter = chunk.iter;
if (!iter) return;
bool keep_going = true;
while (keep_going) {
chunk = cd->get_data(mime, iter, cd->ctype);
if (!chunk.sz) break;
if (!write_all(fd, chunk.data, chunk.sz)) keep_going = false;
if (chunk.free) chunk.free((void*)chunk.free_data);
} }
cd->get_data(NULL, iter, cd->ctype);
}
static void _glfwSendClipboardText(void *data UNUSED, struct wl_data_source *data_source UNUSED, const char *mime_type, int fd) {
send_clipboard_data(&_glfw.clipboard, mime_type, fd);
close(fd); close(fd);
} }
static void _glfwSendClipboardText(void *data UNUSED, struct wl_data_source *data_source UNUSED, const char *mime_type UNUSED, int fd) {
send_text(_glfw.wl.clipboardString, fd);
}
static void _glfwSendPrimarySelectionText(void *data UNUSED, struct zwp_primary_selection_source_v1 *primary_selection_source UNUSED, static void _glfwSendPrimarySelectionText(void *data UNUSED, struct zwp_primary_selection_source_v1 *primary_selection_source UNUSED,
const char *mime_type UNUSED, int fd) { const char *mime_type, int fd) {
send_text(_glfw.wl.primarySelectionString, fd); send_clipboard_data(&_glfw.primary, mime_type, fd);
close(fd);
} }
static char* read_offer_string(int data_pipe, size_t *data_sz) { static void
read_offer(int data_pipe, GLFWclipboardwritedatafun write_data, void *object) {
wl_display_flush(_glfw.wl.display); wl_display_flush(_glfw.wl.display);
size_t capacity = 0;
char *buf = NULL;
*data_sz = 0;
struct pollfd fds; struct pollfd fds;
fds.fd = data_pipe; fds.fd = data_pipe;
fds.events = POLLIN; fds.events = POLLIN;
monotonic_t start = glfwGetTime(); monotonic_t start = glfwGetTime();
#define bail(...) { \ #define bail(...) { \
_glfwInputError(GLFW_PLATFORM_ERROR, __VA_ARGS__); \ _glfwInputError(GLFW_PLATFORM_ERROR, __VA_ARGS__); \
free(buf); buf = NULL; \
close(data_pipe); \ close(data_pipe); \
return NULL; \ return; \
} }
char buf[8192];
while (glfwGetTime() - start < s_to_monotonic_t(2ll)) { while (glfwGetTime() - start < s_to_monotonic_t(2ll)) {
int ret = poll(&fds, 1, 2000); int ret = poll(&fds, 1, 2000);
if (ret == -1) { if (ret == -1) {
@ -1503,34 +1518,65 @@ static char* read_offer_string(int data_pipe, size_t *data_sz) {
if (!ret) { if (!ret) {
bail("Wayland: Failed to read clipboard data from pipe (timed out)"); bail("Wayland: Failed to read clipboard data from pipe (timed out)");
} }
if (capacity <= *data_sz || capacity - *data_sz <= 64) { ret = read(data_pipe, buf, sizeof(buf));
capacity += 4096;
buf = realloc(buf, capacity);
if (!buf) {
bail("Wayland: Failed to allocate memory to read clipboard data");
}
}
ret = read(data_pipe, buf + *data_sz, capacity - *data_sz - 1);
if (ret == -1) { if (ret == -1) {
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue; if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue;
bail("Wayland: Failed to read clipboard data from pipe with error: %s", strerror(errno)); bail("Wayland: Failed to read clipboard data from pipe with error: %s", strerror(errno));
} }
if (ret == 0) { close(data_pipe); buf[*data_sz] = 0; return buf; } if (ret == 0) { close(data_pipe); return; }
*data_sz += ret; if (!write_data(object, buf, ret)) bail("Wayland: call to write_data() failed with data from data offer");
start = glfwGetTime(); start = glfwGetTime();
} }
bail("Wayland: Failed to read clipboard data from pipe (timed out)"); bail("Wayland: Failed to read clipboard data from pipe (timed out)");
#undef bail #undef bail
} }
static char* read_primary_selection_offer(struct zwp_primary_selection_offer_v1 *primary_selection_offer, const char *mime) {
typedef struct chunked_writer {
char *buf; size_t sz, cap;
} chunked_writer;
static bool
write_chunk(void *object, const char *data, size_t sz) {
chunked_writer *cw = object;
if (cw->cap < cw->sz + sz) {
cw->cap = MAX(cw->cap * 2, cw->sz + 8*sz);
cw->buf = realloc(cw->buf, cw->cap);
}
memcpy(cw->buf + cw->sz, data, sz);
cw->sz += sz;
return true;
}
static char*
read_offer_string(int data_pipe, size_t *sz) {
chunked_writer cw = {0};
read_offer(data_pipe, write_chunk, &cw);
if (cw.buf) {
*sz = cw.sz;
return cw.buf;
}
*sz = 0;
return NULL;
}
static void
read_clipboard_data_offer(struct wl_data_offer *data_offer, const char *mime, GLFWclipboardwritedatafun write_data, void *object) {
int pipefd[2]; int pipefd[2];
if (pipe2(pipefd, O_CLOEXEC) != 0) return NULL; if (pipe2(pipefd, O_CLOEXEC) != 0) return;
wl_data_offer_receive(data_offer, mime, pipefd[1]);
close(pipefd[1]);
read_offer(pipefd[0], write_data, object);
}
static void
read_primary_selection_offer(struct zwp_primary_selection_offer_v1 *primary_selection_offer, const char *mime, GLFWclipboardwritedatafun write_data, void *object) {
int pipefd[2];
if (pipe2(pipefd, O_CLOEXEC) != 0) return;
zwp_primary_selection_offer_v1_receive(primary_selection_offer, mime, pipefd[1]); zwp_primary_selection_offer_v1_receive(primary_selection_offer, mime, pipefd[1]);
close(pipefd[1]); close(pipefd[1]);
size_t sz = 0; read_offer(pipefd[0], write_data, object);
return read_offer_string(pipefd[0], &sz);
} }
static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime, size_t *sz) { static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime, size_t *sz) {
@ -1542,24 +1588,24 @@ static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime,
} }
static void data_source_canceled(void *data UNUSED, struct wl_data_source *wl_data_source) { static void data_source_canceled(void *data UNUSED, struct wl_data_source *wl_data_source) {
if (_glfw.wl.dataSourceForClipboard == wl_data_source) if (_glfw.wl.dataSourceForClipboard == wl_data_source) {
_glfw.wl.dataSourceForClipboard = NULL; _glfw.wl.dataSourceForClipboard = NULL;
_glfw_free_clipboard_data(&_glfw.clipboard);
}
wl_data_source_destroy(wl_data_source); wl_data_source_destroy(wl_data_source);
} }
static void primary_selection_source_canceled(void *data UNUSED, struct zwp_primary_selection_source_v1 *primary_selection_source) { static void primary_selection_source_canceled(void *data UNUSED, struct zwp_primary_selection_source_v1 *primary_selection_source) {
if (_glfw.wl.dataSourceForPrimarySelection == primary_selection_source) if (_glfw.wl.dataSourceForPrimarySelection == primary_selection_source) {
_glfw.wl.dataSourceForPrimarySelection = NULL; _glfw.wl.dataSourceForPrimarySelection = NULL;
_glfw_free_clipboard_data(&_glfw.primary);
}
zwp_primary_selection_source_v1_destroy(primary_selection_source); zwp_primary_selection_source_v1_destroy(primary_selection_source);
} }
static void data_source_target(void *data UNUSED, struct wl_data_source *wl_data_source UNUSED, const char* mime UNUSED) {
}
static const struct wl_data_source_listener data_source_listener = { static const struct wl_data_source_listener data_source_listener = {
.send = _glfwSendClipboardText, .send = _glfwSendClipboardText,
.cancelled = data_source_canceled, .cancelled = data_source_canceled,
.target = data_source_target,
}; };
static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener = { static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener = {
@ -1836,13 +1882,19 @@ static bool _glfwEnsureDataDevice(void) {
return true; return true;
} }
void _glfwPlatformSetClipboardString(const char* string) typedef void(*add_offer_func)(void*, const char *mime);
{
void
_glfwPlatformSetClipboard(GLFWClipboardType t) {
_GLFWClipboardData *cd = NULL;
void *data_source;
add_offer_func f;
if (t == GLFW_CLIPBOARD) {
if (!_glfwEnsureDataDevice()) return; if (!_glfwEnsureDataDevice()) return;
free(_glfw.wl.clipboardString); cd = &_glfw.clipboard;
_glfw.wl.clipboardString = _glfw_strdup(string); f = (add_offer_func)wl_data_source_offer;
if (_glfw.wl.dataSourceForClipboard) if (_glfw.wl.dataSourceForClipboard) wl_data_source_destroy(_glfw.wl.dataSourceForClipboard);
wl_data_source_destroy(_glfw.wl.dataSourceForClipboard);
_glfw.wl.dataSourceForClipboard = wl_data_device_manager_create_data_source(_glfw.wl.dataDeviceManager); _glfw.wl.dataSourceForClipboard = wl_data_device_manager_create_data_source(_glfw.wl.dataDeviceManager);
if (!_glfw.wl.dataSourceForClipboard) if (!_glfw.wl.dataSourceForClipboard)
{ {
@ -1851,15 +1903,48 @@ void _glfwPlatformSetClipboardString(const char* string)
return; return;
} }
wl_data_source_add_listener(_glfw.wl.dataSourceForClipboard, &data_source_listener, NULL); wl_data_source_add_listener(_glfw.wl.dataSourceForClipboard, &data_source_listener, NULL);
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, clipboard_mime()); data_source = _glfw.wl.dataSourceForClipboard;
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "text/plain"); } else {
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "text/plain;charset=utf-8"); if (!_glfw.wl.primarySelectionDevice) {
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "TEXT"); static bool warned_about_primary_selection_device = false;
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "STRING"); if (!warned_about_primary_selection_device) {
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "UTF8_STRING"); _glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Cannot copy no primary selection device available");
warned_about_primary_selection_device = true;
}
return;
}
cd = &_glfw.primary;
f = (add_offer_func)zwp_primary_selection_source_v1_offer;
if (_glfw.wl.dataSourceForPrimarySelection) zwp_primary_selection_source_v1_destroy(_glfw.wl.dataSourceForPrimarySelection);
_glfw.wl.dataSourceForPrimarySelection = zwp_primary_selection_device_manager_v1_create_source(_glfw.wl.primarySelectionDeviceManager);
if (!_glfw.wl.dataSourceForPrimarySelection)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Cannot copy failed to create primary selection source");
return;
}
zwp_primary_selection_source_v1_add_listener(_glfw.wl.dataSourceForPrimarySelection, &primary_selection_source_listener, NULL);
data_source = _glfw.wl.dataSourceForPrimarySelection;
}
f(data_source, clipboard_mime());
for (size_t i = 0; i < cd->num_mime_types; i++) {
if (strcmp(cd->mime_types[i], "text/plain") == 0) {
f(data_source, "TEXT");
f(data_source, "STRING");
f(data_source, "UTF8_STRING");
f(data_source, "text/plain;charset=utf-8");
}
f(data_source, cd->mime_types[i]);
}
struct wl_callback *callback = wl_display_sync(_glfw.wl.display); struct wl_callback *callback = wl_display_sync(_glfw.wl.display);
if (t == GLFW_CLIPBOARD) {
static const struct wl_callback_listener clipboard_copy_callback_listener = {.done = clipboard_copy_callback_done}; static const struct wl_callback_listener clipboard_copy_callback_listener = {.done = clipboard_copy_callback_done};
wl_callback_add_listener(callback, &clipboard_copy_callback_listener, _glfw.wl.dataSourceForClipboard); wl_callback_add_listener(callback, &clipboard_copy_callback_listener, _glfw.wl.dataSourceForClipboard);
} else {
static const struct wl_callback_listener primary_selection_copy_callback_listener = {.done = primary_selection_copy_callback_done};
wl_callback_add_listener(callback, &primary_selection_copy_callback_listener, _glfw.wl.dataSourceForPrimarySelection);
}
} }
static bool static bool
@ -1877,81 +1962,47 @@ plain_text_mime_for_offer(const _GLFWWaylandDataOffer *d) {
A("text/plain"); A("text/plain");
A("UTF8_STRING"); A("UTF8_STRING");
A("STRING"); A("STRING");
A("TEXT");
#undef A #undef A
return NULL; return NULL;
} }
const char* _glfwPlatformGetClipboardString(void) void
{ _glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
_GLFWWaylandOfferType offer_type = clipboard_type == GLFW_PRIMARY_SELECTION ? PRIMARY_SELECTION : CLIPBOARD;
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) { for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
_GLFWWaylandDataOffer *d = _glfw.wl.dataOffers + i; _GLFWWaylandDataOffer *d = _glfw.wl.dataOffers + i;
if (d->id && d->offer_type == CLIPBOARD) { if (d->id && d->offer_type == offer_type) {
if (d->is_self_offer) return _glfw.wl.clipboardString; if (d->is_self_offer) {
const char *mime = plain_text_mime_for_offer(d); write_data(object, NULL, 1);
if (mime) { return;
free(_glfw.wl.pasteString);
size_t sz = 0;
_glfw.wl.pasteString = read_data_offer(d->id, mime, &sz);
return _glfw.wl.pasteString;
} }
if (mime_type == NULL) {
bool ok = true;
for (size_t o = 0; o < d->mimes_count; o++) {
const char *q = d->mimes[o];
if (strchr(d->mimes[0], '/')) {
if (strcmp(q, clipboard_mime()) == 0) continue;
if (strcmp(q, "text/plain;charset=utf-8") == 0) q = "text/plain";
} else {
if (strcmp(q, "UTF8_STRING") == 0 || strcmp(q, "STRING") == 0 || strcmp(q, "TEXT") == 0) q = "text/plain";
} }
} if (ok) ok = write_data(object, q, strlen(q));
return NULL;
}
void _glfwPlatformSetPrimarySelectionString(const char* string)
{
if (!_glfw.wl.primarySelectionDevice) {
static bool warned_about_primary_selection_device = false;
if (!warned_about_primary_selection_device) {
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Cannot copy no primary selection device available");
warned_about_primary_selection_device = true;
} }
return; return;
} }
if (_glfw.wl.primarySelectionString == string) return; if (strcmp(mime_type, "text/plain") == 0) {
free(_glfw.wl.primarySelectionString); mime_type = plain_text_mime_for_offer(d);
_glfw.wl.primarySelectionString = _glfw_strdup(string); if (!mime_type) return;
if (_glfw.wl.dataSourceForPrimarySelection)
zwp_primary_selection_source_v1_destroy(_glfw.wl.dataSourceForPrimarySelection);
_glfw.wl.dataSourceForPrimarySelection = zwp_primary_selection_device_manager_v1_create_source(_glfw.wl.primarySelectionDeviceManager);
if (!_glfw.wl.dataSourceForPrimarySelection)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Cannot copy failed to create primary selection source");
return;
} }
zwp_primary_selection_source_v1_add_listener(_glfw.wl.dataSourceForPrimarySelection, &primary_selection_source_listener, NULL); if (d->is_primary) {
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, clipboard_mime()); read_primary_selection_offer(d->id, mime_type, write_data, object);
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "text/plain"); } else {
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "text/plain;charset=utf-8"); read_clipboard_data_offer(d->id, mime_type, write_data, object);
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "TEXT"); }
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "STRING"); break;
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "UTF8_STRING");
struct wl_callback *callback = wl_display_sync(_glfw.wl.display);
static const struct wl_callback_listener primary_selection_copy_callback_listener = {.done = primary_selection_copy_callback_done};
wl_callback_add_listener(callback, &primary_selection_copy_callback_listener, _glfw.wl.dataSourceForPrimarySelection);
}
const char* _glfwPlatformGetPrimarySelectionString(void)
{
if (_glfw.wl.dataSourceForPrimarySelection != NULL) return _glfw.wl.primarySelectionString;
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
_GLFWWaylandDataOffer *d = _glfw.wl.dataOffers + i;
if (d->id && d->is_primary && d->offer_type == PRIMARY_SELECTION) {
if (d->is_self_offer) return _glfw.wl.primarySelectionString;
const char *mime = plain_text_mime_for_offer(d);
if (mime) {
free(_glfw.wl.pasteString);
_glfw.wl.pasteString = read_primary_selection_offer(d->id, mime);
return _glfw.wl.pasteString;
} }
} }
}
return NULL;
} }
EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs UNUSED) EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs UNUSED)