diff options
| -rw-r--r-- | docs/guide.md | 6 | ||||
| -rw-r--r-- | include/yasl/yasl.h | 14 | ||||
| -rw-r--r-- | src/common/backend.c | 7 | ||||
| -rw-r--r-- | src/common/backend.h | 2 | ||||
| -rw-r--r-- | src/platform/cocoa/window.m | 7 | ||||
| -rw-r--r-- | src/platform/freedesktop/dbus.c | 21 | ||||
| -rw-r--r-- | src/platform/freedesktop/dbus.h | 5 | ||||
| -rw-r--r-- | src/platform/wayland/backend.c | 9 | ||||
| -rw-r--r-- | src/platform/x11/window.c | 80 | ||||
| -rw-r--r-- | tests/common/test_desktop.c | 53 | ||||
| -rw-r--r-- | tests/x11/test_input.c | 32 | ||||
| -rw-r--r-- | tests/x11/test_state.c | 55 |
12 files changed, 278 insertions, 13 deletions
diff --git a/docs/guide.md b/docs/guide.md index 7f02ab7..2926011 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -222,7 +222,11 @@ with `device` distinguishing mouse, touch, pen, and eraser. `POINTER_DOWN` and or touch contact. Other types leave `button` meaningless. Every pointer event also carries `buttons`, a `YASL_BUTTON_BIT()` mask of the buttons held after that event, tracked from the queue's down and up events. Use it to tell a drag -from a hover. `YASL_EVENT_INPUT_RESET` clears the mask. On Wayland, +from a hover. Count clicks against `yasl_display_double_click_ms()`, the +system interval: XSETTINGS on X11, the KDE or GNOME setting through the +Settings portal on Wayland, and the system value on Cocoa. It falls back to +`YASL_DOUBLE_CLICK_MS_DEFAULT` (400 ms). The query can wait briefly on Linux, so +read it when a window gains focus. `YASL_EVENT_INPUT_RESET` clears the mask. On Wayland, `POINTER_LEAVE` also clears it, because the compositor sends leave during a press only when it takes the pointer away. X11 and Cocoa keep the press across leave and still deliver the release. `POINTER_MOTION` reports `delta`, the device-pixel movement since diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h index 973c39d..7c9bfbd 100644 --- a/include/yasl/yasl.h +++ b/include/yasl/yasl.h @@ -568,6 +568,20 @@ YASL_API void yasl_display_flush(struct yasl_display *d); * For an individual event use its own mods field instead. */ YASL_API int yasl_display_modifiers(const struct yasl_display *d); +/** Double-click interval used when the system reports none. */ +#define YASL_DOUBLE_CLICK_MS_DEFAULT 400 +/** + * Read the system's double-click interval. + * @param d Display, or NULL. + * @return Maximum milliseconds between the clicks of a double click, or + * YASL_DOUBLE_CLICK_MS_DEFAULT when d is NULL or the system reports none. + * X11 reads the XSETTINGS Net/DoubleClickTime value. Wayland, and X11 without + * XSETTINGS, read the KDE or GNOME setting through the XDG Settings portal, + * which needs D-Bus and can wait briefly. Cocoa reads the system interval. + * The user can change it at any time; read it again when a window gains focus + * rather than on every click. + */ +YASL_API int yasl_display_double_click_ms(const struct yasl_display *d); /* Unsupported/invalid selections are no-ops, never clipboard aliases. */ /** * Offer a copy of a NUL-terminated UTF-8 string as a selection. diff --git a/src/common/backend.c b/src/common/backend.c index d35d243..b15233f 100644 --- a/src/common/backend.c +++ b/src/common/backend.c @@ -110,6 +110,13 @@ int yasl_display_modifiers(const struct yasl_display *d) return d->ops->display_modifiers(d); } +int yasl_display_double_click_ms(const struct yasl_display *d) +{ + int ms = d && d->ops->display_double_click_ms ? d->ops->display_double_click_ms(d) : 0; + /* Reject nonsense from misconfigured settings as well as no answer. */ + return ms > 0 && ms <= 10000 ? ms : YASL_DOUBLE_CLICK_MS_DEFAULT; +} + void yasl_display_copy(struct yasl_display *d, enum yasl_selection sel, const char *text) { if (!d || !d->ops->display_copy || !text || diff --git a/src/common/backend.h b/src/common/backend.h index fd60418..96ce9e7 100644 --- a/src/common/backend.h +++ b/src/common/backend.h @@ -57,6 +57,8 @@ struct yasl_backend_ops { int (*display_dispatch_timeout)(struct yasl_display *d, int timeout_ms); void (*display_flush)(struct yasl_display *d); int (*display_modifiers)(const struct yasl_display *d); + /* Optional; returns zero or less when the system reports no interval. */ + int (*display_double_click_ms)(const struct yasl_display *d); void (*display_copy)(struct yasl_display *d, enum yasl_selection sel, const char *text); void (*display_paste)(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *target); bool (*display_drag_start)(struct yasl_display *d, struct yasl_surface *origin, const struct yasl_drag_image *image, const struct yasl_drag_listener *l, void *ud); diff --git a/src/platform/cocoa/window.m b/src/platform/cocoa/window.m index 75dce54..1efff59 100644 --- a/src/platform/cocoa/window.m +++ b/src/platform/cocoa/window.m @@ -2047,6 +2047,12 @@ static void impl_display_flush(struct yasl_display *d) cocoa_display_flush((struct cocoa_display *)d); } +static int impl_display_double_click_ms(const struct yasl_display *d) +{ + (void)d; + return (int)lround([NSEvent doubleClickInterval] * 1000.0); +} + static int impl_display_modifiers(const struct yasl_display *d) { return cocoa_display_modifiers((const struct cocoa_display *)d); @@ -2231,6 +2237,7 @@ const struct yasl_backend_ops yasl_cocoa_backend = { .display_dispatch_timeout = impl_display_dispatch_timeout, .display_flush = impl_display_flush, .display_modifiers = impl_display_modifiers, + .display_double_click_ms = impl_display_double_click_ms, .display_copy = impl_display_copy, .display_paste = impl_display_paste, .display_drag_start = impl_display_drag_start, diff --git a/src/platform/freedesktop/dbus.c b/src/platform/freedesktop/dbus.c index abca900..484b987 100644 --- a/src/platform/freedesktop/dbus.c +++ b/src/platform/freedesktop/dbus.c @@ -4,6 +4,7 @@ #if YASL_HAS_DBUS #include <dbus/dbus.h> +#include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -116,6 +117,15 @@ static bool iter_int(DBusMessageIter *it, int64_t *out) } DBusBasicValue v; switch (dbus_message_iter_get_arg_type(it)) { + case DBUS_TYPE_STRING: { + dbus_message_iter_get_basic(it, &v); + char *end; + errno = 0; + long long n = strtoll(v.str, &end, 10); + if (errno || end == v.str || *end) return false; + *out = n; + return true; + } case DBUS_TYPE_BYTE: dbus_message_iter_get_basic(it, &v); *out = v.byt; return true; case DBUS_TYPE_BOOLEAN: dbus_message_iter_get_basic(it, &v); *out = v.bool_val; return true; case DBUS_TYPE_INT16: dbus_message_iter_get_basic(it, &v); *out = v.i16; return true; @@ -185,3 +195,14 @@ bool portal_read_int(const char *ns, const char *key, int64_t *out) } #endif + +int portal_double_click_ms(void) +{ + /* kdeglobals holds only values the user changed, so a KDE answer is + * deliberate; the GNOME key can be a default from another portal backend. */ + int64_t ms; + if (portal_read_int("org.kde.kdeglobals.KDE", "DoubleClickInterval", &ms) || + portal_read_int("org.gnome.desktop.peripherals.mouse", "double-click", &ms)) + return ms > 0 && ms <= INT32_MAX ? (int)ms : 0; + return 0; +} diff --git a/src/platform/freedesktop/dbus.h b/src/platform/freedesktop/dbus.h index 3b1662b..fd0aeb1 100644 --- a/src/platform/freedesktop/dbus.h +++ b/src/platform/freedesktop/dbus.h @@ -10,7 +10,10 @@ /* Send org.freedesktop.Notifications.Notify without waiting for a reply. */ bool bus_notify(const char *app, const char *title, const char *body); -/* Read an integer setting through the XDG Settings portal, waiting briefly. */ +/* Read an integer setting through the XDG Settings portal, waiting briefly. + * A string holding a decimal integer also counts, as KDE reports them. */ bool portal_read_int(const char *ns, const char *key, int64_t *out); +/* The KDE or GNOME double-click interval from the portal, or zero. */ +int portal_double_click_ms(void); #endif diff --git a/src/platform/wayland/backend.c b/src/platform/wayland/backend.c index c6272ce..ad9ed4c 100644 --- a/src/platform/wayland/backend.c +++ b/src/platform/wayland/backend.c @@ -1,4 +1,5 @@ #include "platform/wayland/internal.h" +#include "platform/freedesktop/dbus.h" static struct yasl_display *impl_display_open(struct yasl_loop *loop, const struct yasl_display_spec *spec) { @@ -31,6 +32,13 @@ static int impl_display_modifiers(const struct yasl_display *d) return wayland_display_modifiers((const struct wayland_display *)d); } +static int impl_display_double_click_ms(const struct yasl_display *d) +{ + /* Wayland has no protocol for it; desktops publish it as a setting. */ + (void)d; + return portal_double_click_ms(); +} + static void impl_display_copy(struct yasl_display *d, enum yasl_selection sel, const char *text) { wayland_display_copy((struct wayland_display *)d, sel, text); @@ -202,6 +210,7 @@ const struct yasl_backend_ops yasl_wayland_backend = { .display_dispatch_timeout = impl_display_dispatch_timeout, .display_flush = impl_display_flush, .display_modifiers = impl_display_modifiers, + .display_double_click_ms = impl_display_double_click_ms, .display_copy = impl_display_copy, .display_paste = impl_display_paste, .display_drag_start = impl_display_drag_start, diff --git a/src/platform/x11/window.c b/src/platform/x11/window.c index be1ff32..724987f 100644 --- a/src/platform/x11/window.c +++ b/src/platform/x11/window.c @@ -6,6 +6,7 @@ #include "keymap.h" #include "common/utf8.h" #include "common/timing.h" +#include "platform/freedesktop/dbus.h" #include <X11/Xatom.h> #include <X11/XKBlib.h> @@ -27,6 +28,7 @@ #include <limits.h> #include <math.h> #include <poll.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> @@ -1804,6 +1806,83 @@ static bool place(struct yasl_surface *base, struct yasl_vector2i at) return true; } +static uint32_t xsettings_card(const unsigned char *p, int size, bool msb) +{ + uint32_t v = 0; + for (int i = 0; i < size; i++) + v |= (uint32_t)p[i] << (8 * (msb ? size - 1 - i : i)); + return v; +} + +/* Find an integer in an _XSETTINGS_SETTINGS blob: a byte-order byte, three + * pad bytes, a serial, a count, then entries of type, pad, name length, name + * padded to four bytes, serial, and a typed value. */ +static bool xsettings_int(const unsigned char *p, size_t len, const char *name, int32_t *out) +{ + if (len < 12) return false; + bool msb = p[0] == MSBFirst; + uint32_t n = xsettings_card(p + 8, 4, msb); + size_t want = strlen(name); + size_t off = 12; + for (uint32_t i = 0; i < n; i++) { + if (len - off < 4) return false; + unsigned type = p[off]; + size_t name_len = xsettings_card(p + off + 2, 2, msb); + size_t padded = (name_len + 3) & ~(size_t)3; + const unsigned char *entry_name = p + off + 4; + off += 4; + if (len - off < padded + 4) return false; + off += padded + 4; + size_t value_len; + if (type == 0) { + value_len = 4; + } else if (type == 1) { + if (len - off < 4) return false; + size_t str_len = xsettings_card(p + off, 4, msb); + if (str_len > len) return false; + value_len = 4 + ((str_len + 3) & ~(size_t)3); + } else if (type == 2) { + value_len = 8; + } else { + return false; + } + if (len - off < value_len) return false; + if (type == 0 && name_len == want && memcmp(entry_name, name, want) == 0) { + *out = (int32_t)xsettings_card(p + off, 4, msb); + return true; + } + off += value_len; + } + return false; +} + +static int double_click_ms(const struct yasl_display *base) +{ + const struct x11_display *d = (const struct x11_display *)base; + if (d->native) { + char name[32]; + snprintf(name, sizeof(name), "_XSETTINGS_S%d", d->screen); + Atom selection = XInternAtom(d->native, name, True); + Atom property = XInternAtom(d->native, "_XSETTINGS_SETTINGS", True); + Window owner = selection ? XGetSelectionOwner(d->native, selection) : None; + Atom actual; + int format; + unsigned long count, left; + unsigned char *data = NULL; + int32_t ms; + if (owner && property && + XGetWindowProperty(d->native, owner, property, 0, 0x10000, False, property, + &actual, &format, &count, &left, &data) == Success && + data && actual == property && format == 8 && + xsettings_int(data, count, "Net/DoubleClickTime", &ms)) { + XFree(data); + return ms; + } + if (data) XFree(data); + } + return portal_double_click_ms(); +} + static bool bell(struct yasl_surface *base) { struct x11_surface *s = (struct x11_surface *)base; @@ -2059,6 +2138,7 @@ const struct yasl_backend_ops yasl_x11_backend = { .surface_drag_carry = drag_carry, .surface_place_under_pointer = place, .surface_bell = bell, + .display_double_click_ms = double_click_ms, .surface_start_interactive_move = interactive_move, .surface_start_interactive_resize = interactive_resize, .surface_show_window_menu = show_window_menu, diff --git a/tests/common/test_desktop.c b/tests/common/test_desktop.c index f65a9cd..89760fe 100644 --- a/tests/common/test_desktop.c +++ b/tests/common/test_desktop.c @@ -1,5 +1,6 @@ #define _GNU_SOURCE #include "common/backend.h" +#include "platform/freedesktop/dbus.h" #include "yasl/desktop.h" #include <assert.h> @@ -142,6 +143,8 @@ static void test_open_uri(const char *dir) static void test_appearance_fallback(void) { + assert(yasl_display_double_click_ms(NULL) == YASL_DOUBLE_CLICK_MS_DEFAULT); + assert(portal_double_click_ms() == 0); struct yasl_display display = {0}; assert(yasl_display_appearance(NULL) == 0); unsetenv("GTK_THEME"); @@ -160,6 +163,8 @@ static void test_appearance_fallback(void) } #if YASL_HAS_DBUS +static dbus_int32_t fake_slot = -1; + /* A fake notification server and Settings portal on a private bus. */ struct fake_bus { DBusConnection *conn; @@ -167,6 +172,7 @@ struct fake_bus { char app[64], title[64], body[64], entry[64]; int notifications; bool read_one; + bool kde; _Atomic bool stop; }; @@ -175,14 +181,28 @@ static void reply_setting(DBusConnection *c, DBusMessage *msg, bool nested) const char *ns = NULL, *key = NULL; dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &ns, DBUS_TYPE_STRING, &key, DBUS_TYPE_INVALID); - dbus_uint32_t value; - if (ns && key && strcmp(ns, "org.freedesktop.appearance") == 0 && - strcmp(key, "color-scheme") == 0) { - value = 1; - } else if (ns && key && strcmp(ns, "org.freedesktop.appearance") == 0 && - strcmp(key, "contrast") == 0) { - value = 1; + struct fake_bus *fb = dbus_connection_get_data(c, fake_slot); + dbus_uint32_t value = 1; + dbus_int32_t gnome_ms = 333; + const char *kde_ms = "250"; + int type = DBUS_TYPE_UINT32; + const void *data = &value; + if (!ns || !key) { + type = DBUS_TYPE_INVALID; + } else if (strcmp(ns, "org.freedesktop.appearance") == 0 && + (strcmp(key, "color-scheme") == 0 || strcmp(key, "contrast") == 0)) { + } else if (strcmp(ns, "org.gnome.desktop.peripherals.mouse") == 0 && + strcmp(key, "double-click") == 0) { + type = DBUS_TYPE_INT32; + data = &gnome_ms; + } else if (fb->kde && strcmp(ns, "org.kde.kdeglobals.KDE") == 0 && + strcmp(key, "DoubleClickInterval") == 0) { + type = DBUS_TYPE_STRING; + data = &kde_ms; } else { + type = DBUS_TYPE_INVALID; + } + if (type == DBUS_TYPE_INVALID) { DBusMessage *err = dbus_message_new_error(msg, "org.freedesktop.portal.Error.NotFound", "Requested setting not found"); dbus_connection_send(c, err, NULL); @@ -191,16 +211,17 @@ static void reply_setting(DBusConnection *c, DBusMessage *msg, bool nested) } DBusMessage *reply = dbus_message_new_method_return(msg); DBusMessageIter it, outer, inner; + const char signature[2] = {(char)type, '\0'}; dbus_message_iter_init_append(reply, &it); if (nested) { dbus_message_iter_open_container(&it, DBUS_TYPE_VARIANT, "v", &outer); - dbus_message_iter_open_container(&outer, DBUS_TYPE_VARIANT, "u", &inner); - dbus_message_iter_append_basic(&inner, DBUS_TYPE_UINT32, &value); + dbus_message_iter_open_container(&outer, DBUS_TYPE_VARIANT, signature, &inner); + dbus_message_iter_append_basic(&inner, type, data); dbus_message_iter_close_container(&outer, &inner); dbus_message_iter_close_container(&it, &outer); } else { - dbus_message_iter_open_container(&it, DBUS_TYPE_VARIANT, "u", &outer); - dbus_message_iter_append_basic(&outer, DBUS_TYPE_UINT32, &value); + dbus_message_iter_open_container(&it, DBUS_TYPE_VARIANT, signature, &outer); + dbus_message_iter_append_basic(&outer, type, data); dbus_message_iter_close_container(&it, &outer); } dbus_connection_send(c, reply, NULL); @@ -280,6 +301,8 @@ static void test_bus(const char *address) pthread_mutex_init(&fb.lock, NULL); fb.conn = dbus_connection_open_private(address, &err); assert(fb.conn && dbus_bus_register(fb.conn, &err)); + assert(dbus_connection_allocate_data_slot(&fake_slot)); + assert(dbus_connection_set_data(fb.conn, fake_slot, &fb, NULL)); assert(dbus_bus_request_name(fb.conn, "org.freedesktop.Notifications", 0, &err) == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER); assert(dbus_bus_request_name(fb.conn, "org.freedesktop.portal.Desktop", 0, &err) == @@ -311,10 +334,18 @@ static void test_bus(const char *address) assert(yasl_display_appearance(&display) == want); unsetenv("GTK_THEME"); + /* A KDE value, sent as a string, wins over the GNOME key. */ + assert(portal_double_click_ms() == 333); + fb.kde = true; + assert(portal_double_click_ms() == 250); + fb.read_one = true; + assert(portal_double_click_ms() == 250); + fb.stop = true; pthread_join(thread, NULL); dbus_connection_close(fb.conn); dbus_connection_unref(fb.conn); + dbus_connection_free_data_slot(&fake_slot); pthread_mutex_destroy(&fb.lock); } #endif diff --git a/tests/x11/test_input.c b/tests/x11/test_input.c index bbcbf45..95943d1 100644 --- a/tests/x11/test_input.c +++ b/tests/x11/test_input.c @@ -65,6 +65,37 @@ static void check_selection_encoding(struct x11_display *d, struct x11_surface * } } +/* Own the XSETTINGS selection with one Net/DoubleClickTime value. */ +static void check_double_click(struct x11_display *d) +{ + /* No settings owner and no portal: the default applies. */ + unsetenv("DBUS_SESSION_BUS_ADDRESS"); + unsetenv("XDG_RUNTIME_DIR"); + assert(yasl_display_double_click_ms(&d->base) == YASL_DOUBLE_CLICK_MS_DEFAULT); + + char name[32]; + snprintf(name, sizeof(name), "_XSETTINGS_S%d", d->screen); + Atom selection = XInternAtom(d->native, name, False); + Atom property = XInternAtom(d->native, "_XSETTINGS_SETTINGS", False); + Window owner = XCreateSimpleWindow(d->native, DefaultRootWindow(d->native), + 0, 0, 1, 1, 0, 0, 0); + XSetSelectionOwner(d->native, selection, owner, CurrentTime); + static const unsigned char blob[] = { + LSBFirst, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 19, 0, 'N', 'e', 't', '/', 'D', 'o', 'u', 'b', 'l', 'e', + 'C', 'l', 'i', 'c', 'k', 'T', 'i', 'm', 'e', 0, 0, 0, 0, 0, + 250, 0, 0, 0, + }; + XChangeProperty(d->native, owner, property, property, 8, PropModeReplace, + blob, sizeof(blob)); + XSync(d->native, False); + assert(yasl_display_double_click_ms(&d->base) == 250); + + XDestroyWindow(d->native, owner); + XSync(d->native, False); + assert(yasl_display_double_click_ms(&d->base) == YASL_DOUBLE_CLICK_MS_DEFAULT); +} + int main(void) { struct yasl_display_spec ds = {.backend = YASL_BACKEND_X11}; @@ -117,6 +148,7 @@ int main(void) XFree(saved); s->ic = saved_ic; check_selection_encoding(d, s); + check_double_click(d); /* Test CSD interactive actions, window menu, and IME caret */ assert(yasl_display_caps(base) & YASL_CAP_CSD); diff --git a/tests/x11/test_state.c b/tests/x11/test_state.c index abdda95..abc0851 100644 --- a/tests/x11/test_state.c +++ b/tests/x11/test_state.c @@ -36,8 +36,63 @@ static void state_changed(struct yasl_surface *surface, uint32_t state, void *ud notifications++; } +/* Build an XSETTINGS blob in either byte order. */ +struct blob { + unsigned char bytes[256]; + size_t len; + bool msb; +}; + +static void put(struct blob *b, uint32_t v, int size) +{ + for (int i = 0; i < size; i++) + b->bytes[b->len++] = (unsigned char)(v >> (8 * (b->msb ? size - 1 - i : i))); +} + +static void put_name(struct blob *b, unsigned type, const char *name) +{ + size_t n = strlen(name); + b->bytes[b->len++] = (unsigned char)type; + b->bytes[b->len++] = 0; + put(b, (uint32_t)n, 2); + memcpy(b->bytes + b->len, name, n); + b->len += (n + 3) & ~(size_t)3; + put(b, 7, 4); /* Last-change serial. */ +} + +static void test_xsettings(void) +{ + for (int order = 0; order < 2; order++) { + struct blob b = {.msb = order == 1}; + b.bytes[0] = b.msb ? MSBFirst : LSBFirst; + b.len = 4; + put(&b, 1, 4); + put(&b, 4, 4); + put_name(&b, 1, "Net/ThemeName"); + put(&b, 5, 4); + memcpy(b.bytes + b.len, "Adwai", 5); + b.len += 8; + put_name(&b, 2, "Net/Color"); + put(&b, 0x1111, 2); put(&b, 0x2222, 2); put(&b, 0x3333, 2); put(&b, 0xffff, 2); + put_name(&b, 0, "Net/DoubleClickTimeX"); + put(&b, 999, 4); + put_name(&b, 0, "Net/DoubleClickTime"); + put(&b, 250, 4); + + int32_t ms = 0; + assert(xsettings_int(b.bytes, b.len, "Net/DoubleClickTime", &ms) && ms == 250); + assert(!xsettings_int(b.bytes, b.len, "Net/DoubleClick", &ms)); + /* A string with that name is not an integer setting. */ + assert(!xsettings_int(b.bytes, b.len, "Net/ThemeName", &ms)); + /* The value is last, so any truncation fails cleanly. */ + for (size_t len = 0; len < b.len; len++) + assert(!xsettings_int(b.bytes, len, "Net/DoubleClickTime", &ms)); + } +} + int main(void) { + test_xsettings(); struct x11_display display = {.base.ops = &yasl_x11_backend, .net_state = 10, .fullscreen = 20, .maximized_h = 21, .maximized_v = 22}; display.native = (Display *)&display; |
