SDL: wayland: Add a DBus message handler for the cursor size and theme properties.

From fd6cb4dc86073a2eee4caa371882498720120dfb Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Wed, 28 Jun 2023 11:42:44 -0400
Subject: [PATCH] wayland: Add a DBus message handler for the cursor size and
 theme properties.

Add a DBus message handler to watch and respond to changes to the system cursor size and theme properties. Upon these settings being changed, a cursor refresh will be triggered so the new changes will take effect immediately, without the cursor having to leave and re-enter the window surface.
---
 src/video/wayland/SDL_waylandmouse.c | 198 +++++++++++++++++++--------
 1 file changed, 139 insertions(+), 59 deletions(-)

diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c
index a991c8017892..7749bd5b87d1 100644
--- a/src/video/wayland/SDL_waylandmouse.c
+++ b/src/video/wayland/SDL_waylandmouse.c
@@ -58,19 +58,30 @@ typedef struct
     void *shm_data;
 } Wayland_CursorData;
 
+static int dbus_cursor_size;
+static char *dbus_cursor_theme;
+
 #ifdef SDL_USE_LIBDBUS
 
 #include "../../core/linux/SDL_dbus.h"
 
-static DBusMessage *wayland_read_dbus_setting(SDL_DBusContext *dbus, const char *key)
+#define CURSOR_NODE "org.freedesktop.portal.Desktop"
+#define CURSOR_PATH "/org/freedesktop/portal/desktop"
+#define CURSOR_INTERFACE "org.freedesktop.portal.Settings"
+#define CURSOR_NAMESPACE "org.gnome.desktop.interface"
+#define CURSOR_SIGNAL_NAME "SettingChanged"
+#define CURSOR_SIZE_KEY "cursor-size"
+#define CURSOR_THEME_KEY "cursor-theme"
+
+static DBusMessage *Wayland_ReadDBusProperty(SDL_DBusContext *dbus, const char *key)
 {
     static const char *iface = "org.gnome.desktop.interface";
 
     DBusMessage *reply = NULL;
-    DBusMessage *msg = dbus->message_new_method_call("org.freedesktop.portal.Desktop",  /* Node */
-                                                     "/org/freedesktop/portal/desktop", /* Path */
-                                                     "org.freedesktop.portal.Settings", /* Interface */
-                                                     "Read");                           /* Method */
+    DBusMessage *msg = dbus->message_new_method_call(CURSOR_NODE,
+                                                     CURSOR_PATH,
+                                                     CURSOR_INTERFACE,
+                                                     "Read"); /* Method */
 
     if (msg) {
         if (dbus->message_append_args(msg, DBUS_TYPE_STRING, &iface, DBUS_TYPE_STRING, &key, DBUS_TYPE_INVALID)) {
@@ -82,7 +93,7 @@ static DBusMessage *wayland_read_dbus_setting(SDL_DBusContext *dbus, const char
     return reply;
 }
 
-static SDL_bool wayland_parse_dbus_reply(SDL_DBusContext *dbus, DBusMessage *reply, int type, void *value)
+static SDL_bool Wayland_ParseDBusReply(SDL_DBusContext *dbus, DBusMessage *reply, int type, void *value)
 {
     DBusMessageIter iter[3];
 
@@ -106,50 +117,127 @@ static SDL_bool wayland_parse_dbus_reply(SDL_DBusContext *dbus, DBusMessage *rep
     return SDL_TRUE;
 }
 
-static SDL_bool wayland_dbus_read_cursor_size(int *size)
+static DBusHandlerResult Wayland_DBusCursorMessageFilter(DBusConnection *conn, DBusMessage *msg, void *data)
 {
-    static const char *cursor_size_value = "cursor-size";
-
-    DBusMessage *reply;
     SDL_DBusContext *dbus = SDL_DBus_GetContext();
 
-    if (dbus == NULL || size == NULL) {
-        return SDL_FALSE;
-    }
+    if (dbus->message_is_signal(msg, CURSOR_INTERFACE, CURSOR_SIGNAL_NAME)) {
+        DBusMessageIter signal_iter, variant_iter;
+        const char *namespace, *key;
 
-    if ((reply = wayland_read_dbus_setting(dbus, cursor_size_value))) {
-        if (wayland_parse_dbus_reply(dbus, reply, DBUS_TYPE_INT32, size)) {
-            dbus->message_unref(reply);
-            return SDL_TRUE;
+        dbus->message_iter_init(msg, &signal_iter);
+        /* Check if the parameters are what we expect */
+        if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) {
+            goto not_our_signal;
         }
-        dbus->message_unref(reply);
+        dbus->message_iter_get_basic(&signal_iter, &namespace);
+        if (SDL_strcmp(CURSOR_NAMESPACE, namespace) != 0) {
+            goto not_our_signal;
+        }
+        if (!dbus->message_iter_next(&signal_iter)) {
+            goto not_our_signal;
+        }
+        if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_STRING) {
+            goto not_our_signal;
+        }
+        dbus->message_iter_get_basic(&signal_iter, &key);
+        if (SDL_strcmp(CURSOR_SIZE_KEY, key) == 0) {
+            int new_cursor_size;
+
+            if (!dbus->message_iter_next(&signal_iter)) {
+                goto not_our_signal;
+            }
+            if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_VARIANT) {
+                goto not_our_signal;
+            }
+            dbus->message_iter_recurse(&signal_iter, &variant_iter);
+            if (dbus->message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_INT32) {
+                goto not_our_signal;
+            }
+            dbus->message_iter_get_basic(&variant_iter, &new_cursor_size);
+
+            if (dbus_cursor_size != new_cursor_size) {
+                dbus_cursor_size = new_cursor_size;
+                SDL_SetCursor(NULL); /* Force cursor update */
+            }
+        } else if (SDL_strcmp(CURSOR_THEME_KEY, key) == 0) {
+            const char *new_cursor_theme = NULL;
+
+            if (!dbus->message_iter_next(&signal_iter)) {
+                goto not_our_signal;
+            }
+            if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_VARIANT) {
+                goto not_our_signal;
+            }
+            dbus->message_iter_recurse(&signal_iter, &variant_iter);
+            if (dbus->message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_STRING) {
+                goto not_our_signal;
+            }
+            dbus->message_iter_get_basic(&variant_iter, &new_cursor_theme);
+
+            if (!dbus_cursor_theme || !new_cursor_theme || SDL_strcmp(dbus_cursor_theme, new_cursor_theme) != 0) {
+                SDL_free(dbus_cursor_theme);
+                if (new_cursor_theme) {
+                    dbus_cursor_theme = SDL_strdup(new_cursor_theme);
+                    SDL_SetCursor(NULL); /* Force cursor update */
+                } else {
+                    dbus_cursor_theme = NULL;
+                }
+            }
+        } else {
+            goto not_our_signal;
+        }
+
+        return DBUS_HANDLER_RESULT_HANDLED;
     }
 
-    return SDL_FALSE;
+not_our_signal:
+    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
 }
 
-static SDL_bool wayland_dbus_read_cursor_theme(char **theme)
+static void Wayland_DBusInitCursorProperties()
 {
-    static const char *cursor_theme_value = "cursor-theme";
-
     DBusMessage *reply;
     SDL_DBusContext *dbus = SDL_DBus_GetContext();
+    SDL_bool add_filter = SDL_FALSE;
 
-    if (dbus == NULL || theme == NULL) {
-        return SDL_FALSE;
+    if (dbus == NULL) {
+        return;
     }
 
-    if ((reply = wayland_read_dbus_setting(dbus, cursor_theme_value))) {
-        const char *temp;
-        if (wayland_parse_dbus_reply(dbus, reply, DBUS_TYPE_STRING, &temp)) {
-            *theme = SDL_strdup(temp);
-            dbus->message_unref(reply);
-            return SDL_TRUE;
+    if ((reply = Wayland_ReadDBusProperty(dbus, CURSOR_SIZE_KEY))) {
+        if (Wayland_ParseDBusReply(dbus, reply, DBUS_TYPE_INT32, &dbus_cursor_size)) {
+            add_filter = SDL_TRUE;
         }
         dbus->message_unref(reply);
     }
 
-    return SDL_FALSE;
+    if ((reply = Wayland_ReadDBusProperty(dbus, CURSOR_THEME_KEY))) {
+        const char *temp = NULL;
+        if (Wayland_ParseDBusReply(dbus, reply, DBUS_TYPE_STRING, &temp)) {
+            add_filter = SDL_TRUE;
+
+            if (temp) {
+                dbus_cursor_theme = SDL_strdup(temp);
+            }
+        }
+        dbus->message_unref(reply);
+    }
+
+    /* Only add the filter if at least one of the settings we want is present. */
+    if (add_filter) {
+        dbus->bus_add_match(dbus->session_conn,
+                            "type='signal', interface='"CURSOR_INTERFACE"',"
+                            "member='"CURSOR_SIGNAL_NAME"', arg0='"CURSOR_NAMESPACE"'", NULL);
+        dbus->connection_add_filter(dbus->session_conn, &Wayland_DBusCursorMessageFilter, NULL, NULL);
+        dbus->connection_flush(dbus->session_conn);
+    }
+}
+
+static void Wayland_DBusFinishCursorProperties()
+{
+    SDL_free(dbus_cursor_theme);
+    dbus_cursor_theme = NULL;
 }
 
 #endif
@@ -159,29 +247,19 @@ static SDL_bool wayland_get_system_cursor(SDL_VideoData *vdata, Wayland_CursorDa
     struct wl_cursor_theme *theme = NULL;
     struct wl_cursor *cursor;
 
-    char *xcursor_size;
-    int size = 0;
+    int size = dbus_cursor_size;
 
     SDL_Window *focus;
     SDL_WindowData *focusdata;
     int i;
 
-    /*
-     * GNOME based desktops expose the cursor size and theme via the
-     * org.freedesktop.portal.Settings interface of the xdg-desktop portal.
-     * Try XCURSOR_SIZE and XCURSOR_THEME first, so user specified sizes and
-     * themes take precedence over all, then try D-Bus if the envvar isn't
-     * set, then fall back to the defaults if none of the preceding values
-     * are available or valid.
-     */
-    if ((xcursor_size = SDL_getenv("XCURSOR_SIZE"))) {
-        size = SDL_atoi(xcursor_size);
-    }
-#ifdef SDL_USE_LIBDBUS
+    /* Fallback envvar if the DBus properties don't exist */
     if (size <= 0) {
-        wayland_dbus_read_cursor_size(&size);
+        const char *xcursor_size = SDL_getenv("XCURSOR_SIZE");
+        if (xcursor_size) {
+            size = SDL_atoi(xcursor_size);
+        }
     }
-#endif
     if (size <= 0) {
         size = 24;
     }
@@ -203,8 +281,7 @@ static SDL_bool wayland_get_system_cursor(SDL_VideoData *vdata, Wayland_CursorDa
         }
     }
     if (theme == NULL) {
-        char *xcursor_theme = NULL;
-        SDL_bool free_theme_str = SDL_FALSE;
+        const char *xcursor_theme = dbus_cursor_theme;
 
         vdata->cursor_themes = SDL_realloc(vdata->cursor_themes,
                                            sizeof(SDL_WaylandCursorTheme) * (vdata->num_cursor_themes + 1));
@@ -212,20 +289,15 @@ static SDL_bool wayland_get_system_cursor(SDL_VideoData *vdata, Wayland_CursorDa
             SDL_OutOfMemory();
             return SDL_FALSE;
         }
-        xcursor_theme = SDL_getenv("XCURSOR_THEME");
-#ifdef SDL_USE_LIBDBUS
-        if (xcursor_theme == NULL) {
-            /* Allocates the string with SDL_strdup, which must be freed. */
-            free_theme_str = wayland_dbus_read_cursor_theme(&xcursor_theme);
+
+        /* Fallback envvar if the DBus properties don't exist */
+        if (!xcursor_theme) {
+            xcursor_theme = SDL_getenv("XCURSOR_THEME");
         }
-#endif
+
         theme = WAYLAND_wl_cursor_theme_load(xcursor_theme, size, vdata->shm);
         vdata->cursor_themes[vdata->num_cursor_themes].size = size;
         vdata->cursor_themes[vdata->num_cursor_themes++].theme = theme;
-
-        if (free_theme_str) {
-            SDL_free(xcursor_theme);
-        }
     }
 
     /* Next, find the cursor from the theme... */
@@ -684,6 +756,10 @@ void Wayland_InitMouse(void)
     input->relative_mode_override = SDL_FALSE;
     input->cursor_visible = SDL_TRUE;
 
+#ifdef SDL_USE_LIBDBUS
+    Wayland_DBusInitCursorProperties();
+#endif
+
     SDL_SetDefaultCursor(Wayland_CreateDefaultCursor());
 
     SDL_AddHintCallback(SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP,
@@ -701,6 +777,10 @@ void Wayland_FiniMouse(SDL_VideoData *data)
     SDL_free(data->cursor_themes);
     data->cursor_themes = NULL;
 
+#ifdef SDL_USE_LIBDBUS
+    Wayland_DBusFinishCursorProperties();
+#endif
+
     SDL_DelHintCallback(SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP,
                         Wayland_EmulateMouseWarpChanged, input);
 }