diff --git a/CMakeLists.txt b/CMakeLists.txt index ef55c51740c99..c390f2932fcd1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1732,6 +1732,7 @@ elseif(UNIX AND NOT APPLE AND NOT RISCOS AND NOT HAIKU) sdl_sources( "${SDL3_SOURCE_DIR}/src/core/linux/SDL_dbus.c" "${SDL3_SOURCE_DIR}/src/core/linux/SDL_system_theme.c" + "${SDL3_SOURCE_DIR}/src/core/linux/SDL_system_preferences.c" ) endif() diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index 1323e9f0b6445..41e7a95a3abb5 100644 --- a/include/SDL3/SDL_events.h +++ b/include/SDL3/SDL_events.h @@ -118,6 +118,11 @@ typedef enum SDL_EventType SDL_EVENT_SYSTEM_THEME_CHANGED, /**< The system theme changed */ + SDL_EVENT_SYSTEM_PREFERENCE_CHANGED, /**< A system preference setting changed */ + SDL_EVENT_SYSTEM_TEXT_SCALE_CHANGED, /**< The text scale changed */ + SDL_EVENT_SYSTEM_CURSOR_SCALE_CHANGED, /**< The cursor scale changed */ + SDL_EVENT_SYSTEM_ACCENT_COLOR_CHANGED, /**< The accent color changed */ + /* Display events */ /* 0x150 was SDL_DISPLAYEVENT, reserve the number for sdl2-compat */ SDL_EVENT_DISPLAY_ORIENTATION = 0x151, /**< Display orientation has changed to data1 */ @@ -925,6 +930,23 @@ typedef struct SDL_ClipboardEvent const char **mime_types; /**< current mime types */ } SDL_ClipboardEvent; +/** + * An event triggered when a system preference has changed (event.pref.*) + * + * Note that some platforms may provide certain settings, but not allow + * listening to changes; as such, there may be certain preferences that can be + * fetched but that won't produce events when they are changed. + * + * \since This struct is available since SDL 3.4.0. + */ +typedef struct SDL_PreferenceEvent +{ + SDL_EventType type; /**< SDL_EVENT_SYSTEM_PREFERENCE_CHANGED */ + Uint32 reserved; + Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ + SDL_SystemPreference pref; /**< The preference setting that changed */ +} SDL_PreferenceEvent; + /** * Sensor event structure (event.sensor.*) * @@ -1023,6 +1045,7 @@ typedef union SDL_Event SDL_RenderEvent render; /**< Render event data */ SDL_DropEvent drop; /**< Drag and drop event data */ SDL_ClipboardEvent clipboard; /**< Clipboard event data */ + SDL_PreferenceEvent pref; /**< Preference event data */ /* This is necessary for ABI compatibility between Visual C++ and GCC. Visual C++ will respect the push pack pragma and use 52 bytes (size of diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index a7afc3267b324..31c88ae029a08 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -570,6 +570,131 @@ extern SDL_DECLSPEC const char * SDLCALL SDL_GetCurrentVideoDriver(void); */ extern SDL_DECLSPEC SDL_SystemTheme SDLCALL SDL_GetSystemTheme(void); +/** + * An enumeration of various boolean system preferences. + * + * Some systems provide a variety of accessibility options that allow users to + * adapt their environment to various conditions. + * + * The preference names have been chosen so that `true` indicate a positive + * preference, whereas `false` indicate no preference. Some systems describe + * their preferences negatively, like "Enable animations" on Windows, which is + * `true` by default (no preference) and `false` when the user wants to disable + * animations. In these situations, SDL inverts the preferences so that `false` + * correspond to the situation where no particular change is needed. + * + * \since This enum is available since SDL 3.4.0. + * + * \sa SDL_GetSystemPreference + */ +typedef enum SDL_SystemPreference +{ + SDL_SYSTEM_PREFERENCE_REDUCED_MOTION, /**< Disable smooth graphical transitions */ + SDL_SYSTEM_PREFERENCE_REDUCED_TRANSPARENCY, /**< Reduce usage of semi-transparent objects */ + SDL_SYSTEM_PREFERENCE_HIGH_CONTRAST, /**< Use extreme color differences between different elements of the interface */ + SDL_SYSTEM_PREFERENCE_COLORBLIND, /**< Add shape-based distinction between color-coded elements, for example "0" and "1" on switches */ + SDL_SYSTEM_PREFERENCE_PERSIST_SCROLLBARS, /**< Always show scrollbars, don't hide them after a few seconds of inactivity */ + SDL_SYSTEM_PREFERENCE_SCREEN_READER, /**< A screen reader (text-to-speech OR braille) is currently active */ +} SDL_SystemPreference; + +/** + * Get whether or not a certain system preference was enabled by the user. + * + * Some preferences may not be supported on some platforms; this function will + * return false by default in this case. The preference names have been chosen + * so that `true` indicate a positive preference, whereas `false` indicate no + * preference. + * + * This setting will emit a `SDL_EVENT_SYSTEM_PREFERENCE_CHANGED` when the user + * updates the corresponding preference in their settings or accessibility app. + * Note that some platforms may provide certain settings, but not allow + * listening to changes; as such, there may be certain preferences that can be + * fetched but that won't produce events when they are changed. + * + * \param preference the preference to be fetched. + * \returns true if the user enabled the system preference; false if the user + * did not activate the setting, or the setting doesn't exist on the + * current platform, or an error occured. + * + * \threadsafety This function should only be called on the main thread. + * + * \since This function is available since SDL 3.4.0. + * + * \sa SDL_SystemPreference + * \sa SDL_PreferenceEvent + */ +extern SDL_DECLSPEC bool SDLCALL SDL_GetSystemPreference(SDL_SystemPreference preference); + +/** + * Get the system's accent color, as chosen by the user. + * + * If the current system does not have an accent color, false is returned and + * the struct is unaffected. + * + * This setting will emit a `SDL_EVENT_SYSTEM_ACCENT_COLOR_CHANGED` when the + * user updates the corresponding preference in their settings or accessibility + * app. Note that some platforms may provide certain settings, but not allow + * listening to changes; as such, there may be certain preferences that can be + * fetched but that won't produce events when they are changed. + * + * \param color a pointer to a struct to be filled with the color info. The + * alpha channel corresponds to the value returned by operating + * system, which is not necessarily equivalent to SDL_ALPHA_OPAQUE. + * \returns true on success or false on failure; call SDL_GetError() for more + * information. + * + * \threadsafety This function should only be called on the main thread. + * + * \since This function is available since SDL 3.4.0. + */ +extern SDL_DECLSPEC bool SDLCALL SDL_GetSystemAccentColor(SDL_Color *color); + +/** + * Get the scale factor for text, as set by the user for their system. + * + * Some systems do not have a notion of text scale factor, but instead provide + * a base font size. In this case, SDL calculates a scaling factor by dividing + * the given font size by 16 pixels. + * + * If the system does not have a setting to scale the font, 1 is returned. + * + * This setting will emit a `SDL_EVENT_SYSTEM_TEXT_SCALE_CHANGED` when the user + * updates the corresponding preference in their settings or accessibility app. + * Note that some platforms may provide certain settings, but not allow + * listening to changes; as such, there may be certain preferences that can be + * fetched but that won't produce events when they are changed. + * + * \returns the preferred scale for text; a scale of 1 means no scaling. + * + * \threadsafety This function should only be called on the main thread. + * + * \since This function is available since SDL 3.4.0. + */ +extern SDL_DECLSPEC float SDLCALL SDL_GetSystemTextScale(void); + +/** + * Get the scale factor for the cursor, as set by the user for their system. + * + * Some systems do not have a notion of cursor scale factor, but instead provide + * a base cursor size. In this case, SDL calculates a scaling factor by dividing + * the given cursor lateral size by 32 pixels. + * + * If the system does not have a setting to scale the cursor, 1 is returned. + * + * This setting will emit a `SDL_EVENT_SYSTEM_CURSOR_SCALE_CHANGED` when the + * user updates the corresponding preference in their settings or accessibility + * app. Note that some platforms may provide certain settings, but not allow + * listening to changes; as such, there may be certain preferences that can be + * fetched but that won't produce events when they are changed. + * + * \returns the preferred scale for the cursor; a scale of 1 means no scaling. + * + * \threadsafety This function should only be called on the main thread. + * + * \since This function is available since SDL 3.4.0. + */ +extern SDL_DECLSPEC float SDLCALL SDL_GetSystemCursorScale(void); + /** * Get a list of currently connected displays. * diff --git a/src/core/linux/SDL_system_preferences.c b/src/core/linux/SDL_system_preferences.c new file mode 100644 index 0000000000000..2a3324f91b733 --- /dev/null +++ b/src/core/linux/SDL_system_preferences.c @@ -0,0 +1,533 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "SDL_dbus.h" +#include "../../events/SDL_events_c.h" + +#include + +#define PORTAL_DESTINATION "org.freedesktop.portal.Desktop" +#define PORTAL_PATH "/org/freedesktop/portal/desktop" +#define PORTAL_INTERFACE "org.freedesktop.portal.Settings" +#define PORTAL_METHOD "ReadOne" + +#define SIGNAL_INTERFACE "org.freedesktop.portal.Settings" +#define SIGNAL_NAME "SettingChanged" +// Signal namespace and key will vary + +typedef struct SystemPrefData +{ + Uint32 contrast; + Uint32 animations; + Uint32 shapes; + Uint32 hide_scrollbars; + Uint32 cursor_size; + Uint32 screen_reader; + double text_scale; +} SystemPrefData; + +static SystemPrefData system_pref_data; + +typedef struct +{ + const char *destination; + const char *path; + const char *interface; + const char *method; + const char *namespace; + const char *key; + + const char *signal_interface; + const char *signal_name; + const char *signal_args[3]; + const bool check_key; + + const SDL_EventType event_type; + const SDL_SystemPreference event_pref; + + void *setting; +} Property; + +// To be added: +// org.gtk.Settings /org/gtk/Settings/ org.gtk.Settings.EnableAnimations +// org.gnome.SettingsDaemon.XSettings /org/gtk/Settings/ org.gtk.Settings.EnableAnimations +static const Property props[] = { + { + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Settings", + "ReadOne", + "org.freedesktop.appearance", + "contrast", + "org.freedesktop.portal.Settings", + "SettingChanged", + { "org.freedesktop.appearance", "contrast", NULL }, + true, + SDL_EVENT_SYSTEM_PREFERENCE_CHANGED, + SDL_SYSTEM_PREFERENCE_HIGH_CONTRAST, + &system_pref_data.contrast, + }, + { + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Settings", + "ReadOne", + "org.gnome.desktop.interface", + "enable-animations", + "org.freedesktop.portal.Settings", + "SettingChanged", + { "org.gnome.desktop.interface", "enable-animations", NULL }, + true, + SDL_EVENT_SYSTEM_PREFERENCE_CHANGED, + SDL_SYSTEM_PREFERENCE_REDUCED_MOTION, + &system_pref_data.animations, + }, + { + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Settings", + "ReadOne", + "org.gnome.desktop.a11y.interface", + "show-status-shapes", + "org.freedesktop.portal.Settings", + "SettingChanged", + { "org.gnome.desktop.a11y.interface", "show-status-shapes", NULL }, + true, + SDL_EVENT_SYSTEM_PREFERENCE_CHANGED, + SDL_SYSTEM_PREFERENCE_COLORBLIND, + &system_pref_data.shapes, + }, + { + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Settings", + "ReadOne", + "org.gnome.desktop.interface", + "overlay-scrolling", + "org.freedesktop.portal.Settings", + "SettingChanged", + { "org.gnome.desktop.interface", "overlay-scrolling", NULL }, + true, + SDL_EVENT_SYSTEM_PREFERENCE_CHANGED, + SDL_SYSTEM_PREFERENCE_PERSIST_SCROLLBARS, + &system_pref_data.hide_scrollbars, + }, + { + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Settings", + "ReadOne", + "org.gnome.desktop.interface", + "cursor-size", + "org.freedesktop.portal.Settings", + "SettingChanged", + { "org.gnome.desktop.interface", "cursor-size", NULL }, + true, + SDL_EVENT_SYSTEM_CURSOR_SCALE_CHANGED, + 0, + &system_pref_data.cursor_size, + }, + { + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Settings", + "ReadOne", + "org.gnome.desktop.interface", + "text-scaling-factor", + "org.freedesktop.portal.Settings", + "SettingChanged", + { "org.gnome.desktop.interface", "text-scaling-factor", NULL }, + true, + SDL_EVENT_SYSTEM_TEXT_SCALE_CHANGED, + 0, + &system_pref_data.text_scale, + }, + { + "org.a11y.Bus", + "/org/a11y/bus", + "org.freedesktop.DBus.Properties", + "Get", + "org.a11y.Status", + "ScreenReaderEnabled", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + { NULL, NULL, NULL }, + false, + SDL_EVENT_SYSTEM_PREFERENCE_CHANGED, + SDL_SYSTEM_PREFERENCE_SCREEN_READER, + &system_pref_data.screen_reader, + }, +}; + +// FIXME: Type checking for setting Uint32 vs double +static bool DBus_ExtractPref(DBusMessageIter *iter, void *setting) { + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + DBusMessageIter variant_iter; + DBusMessageIter *data_iter; + + // Direct fetch returns UINT32 directly, event sends it wrapped in a variant + if (dbus->message_iter_get_arg_type(iter) == DBUS_TYPE_VARIANT) { + dbus->message_iter_recurse(iter, &variant_iter); + data_iter = &variant_iter; + } else { + data_iter = iter; + } + + switch (dbus->message_iter_get_arg_type(data_iter)) + { + case DBUS_TYPE_UINT32: + case DBUS_TYPE_INT32: + case DBUS_TYPE_BOOLEAN: + dbus->message_iter_get_basic(data_iter, (Uint32 *)setting); + break; + + case DBUS_TYPE_DOUBLE: + dbus->message_iter_get_basic(data_iter, (double *)setting); + break; + + default: + return false; + }; + + return true; +} + +static DBusHandlerResult DBus_MessageFilter(DBusConnection *conn, DBusMessage *msg, void *data) { + SDL_DBusContext *dbus = (SDL_DBusContext *)data; + + for (int i = 0; i < sizeof(props) / sizeof(*props); i++) { + const Property *prop = &props[i]; + + if (dbus->message_is_signal(msg, prop->signal_interface, prop->signal_name)) { + DBusMessageIter signal_iter; + const char *namespace, *key; + + 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) + continue; + + dbus->message_iter_get_basic(&signal_iter, &namespace); + if (SDL_strcmp(prop->namespace, namespace) == 0) { + if (!dbus->message_iter_next(&signal_iter)) + continue; + + if (dbus->message_iter_get_arg_type(&signal_iter) == DBUS_TYPE_STRING) { + dbus->message_iter_get_basic(&signal_iter, &key); + + if (SDL_strcmp(prop->key, key) != 0) + continue; + + if (!dbus->message_iter_next(&signal_iter)) + continue; + + if (!DBus_ExtractPref(&signal_iter, prop->setting)) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (prop->event_type == SDL_EVENT_SYSTEM_PREFERENCE_CHANGED) { + SDL_SendSystemPreferenceChangedEvent(prop->event_pref); + } else { + SDL_SendAppEvent(prop->event_type); + } + + return DBUS_HANDLER_RESULT_HANDLED; + } else if (dbus->message_iter_get_arg_type(&signal_iter) == DBUS_TYPE_ARRAY) { + // This is for PropertiesChanged-based settings, like org.a11y.bus + DBusMessageIter array_iter; + dbus->message_iter_recurse(&signal_iter, &array_iter); + + while (dbus->message_iter_get_arg_type(&array_iter) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dict_entry_iter; + DBusMessageIter value_iter; + + dbus->message_iter_recurse(&array_iter, &dict_entry_iter); + + // Extract the key and value from the dict entry + dbus->message_iter_get_basic(&dict_entry_iter, &key); + + if (SDL_strcmp(key, prop->key) != 0) + goto next_and_continue; + + dbus->message_iter_next(&dict_entry_iter); + dbus->message_iter_recurse(&dict_entry_iter, &value_iter); + + if (!DBus_ExtractPref(&value_iter, prop->setting)) + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + if (prop->event_type == SDL_EVENT_SYSTEM_PREFERENCE_CHANGED) { + SDL_SendSystemPreferenceChangedEvent(prop->event_pref); + } else { + SDL_SendAppEvent(prop->event_type); + } + + return DBUS_HANDLER_RESULT_HANDLED; + +next_and_continue: + dbus->message_iter_next(&array_iter); + } + } else { + continue; + } + } + } + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + + +#if 0 + if (dbus->message_is_signal(msg, SIGNAL_INTERFACE, SIGNAL_NAME)) { + DBusMessageIter signal_iter; + const char *namespace, *key; + + 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_iter_get_basic(&signal_iter, &namespace); + + // FIXME: For every setting outside org.freedesktop.appearance, DBus + // sends two events rather than one. + if (SDL_strcmp("org.freedesktop.appearance", namespace) == 0) { + 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("contrast", key) != 0) + goto not_our_signal; + + if (!dbus->message_iter_next(&signal_iter)) + goto not_our_signal; + + if (!DBus_ExtractPref(&signal_iter, &system_pref_data.contrast)) + goto not_our_signal; + + SDL_SendSystemPreferenceChangedEvent(SDL_SYSTEM_PREFERENCE_HIGH_CONTRAST); + + return DBUS_HANDLER_RESULT_HANDLED; + } else if (SDL_strcmp("org.gnome.desktop.interface", namespace) == 0) { + 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("enable-animations", key) == 0) { + if (!dbus->message_iter_next(&signal_iter)) + goto not_our_signal; + + if (!DBus_ExtractPref(&signal_iter, &system_pref_data.animations)) + goto not_our_signal; + + SDL_SendSystemPreferenceChangedEvent(SDL_SYSTEM_PREFERENCE_REDUCED_MOTION); + + return DBUS_HANDLER_RESULT_HANDLED; + } else if (SDL_strcmp("overlay-scrolling", key) == 0) { + if (!dbus->message_iter_next(&signal_iter)) + goto not_our_signal; + + if (!DBus_ExtractPref(&signal_iter, &system_pref_data.hide_scrollbars)) + goto not_our_signal; + + SDL_SendSystemPreferenceChangedEvent(SDL_SYSTEM_PREFERENCE_PERSIST_SCROLLBARS); + + return DBUS_HANDLER_RESULT_HANDLED; + } else if (SDL_strcmp("cursor-size", key) == 0) { + if (!dbus->message_iter_next(&signal_iter)) + goto not_our_signal; + + if (!DBus_ExtractPref(&signal_iter, &system_pref_data.cursor_size)) + goto not_our_signal; + + SDL_SendAppEvent(SDL_EVENT_SYSTEM_CURSOR_SCALE_CHANGED); + + return DBUS_HANDLER_RESULT_HANDLED; + } else if (SDL_strcmp("text-scaling-factor", key) == 0) { + if (!dbus->message_iter_next(&signal_iter)) + goto not_our_signal; + + if (!DBus_ExtractPref(&signal_iter, &system_pref_data.text_scale)) + goto not_our_signal; + + SDL_SendAppEvent(SDL_EVENT_SYSTEM_TEXT_SCALE_CHANGED); + + return DBUS_HANDLER_RESULT_HANDLED; + } else { + goto not_our_signal; + } + } else if (SDL_strcmp("org.gnome.desktop.a11y.interface", namespace) == 0) { + 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("show-status-shapes", key) == 0) { + if (!dbus->message_iter_next(&signal_iter)) + goto not_our_signal; + + if (!DBus_ExtractPref(&signal_iter, &system_pref_data.shapes)) + goto not_our_signal; + + SDL_SendSystemPreferenceChangedEvent(SDL_SYSTEM_PREFERENCE_COLORBLIND); + + return DBUS_HANDLER_RESULT_HANDLED; + } else { + goto not_our_signal; + } + } else { + goto not_our_signal; + } + } +not_our_signal: + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +#endif +} + +bool Unix_SystemPref_Init(void) +{ + static bool is_init = false; + + if (is_init) { + return true; + } + + SDL_DBusContext *dbus = SDL_DBus_GetContext(); + DBusMessage *msg; + + system_pref_data.contrast = false; + system_pref_data.animations = true; + system_pref_data.shapes = false; + system_pref_data.hide_scrollbars = true; + system_pref_data.screen_reader = false; + system_pref_data.cursor_size = 24; + system_pref_data.text_scale = 1.0f; + + if (!dbus) { + return false; + } + + for (int i = 0; i < sizeof(props) / sizeof(*props); i++) { + const Property *prop = &props[i]; + + msg = dbus->message_new_method_call(prop->destination, prop->path, prop->interface, prop->method); + if (msg) { + if (dbus->message_append_args(msg, DBUS_TYPE_STRING, &prop->namespace, DBUS_TYPE_STRING, &prop->key, DBUS_TYPE_INVALID)) { + DBusMessage *reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, 300, NULL); + if (reply) { + DBusMessageIter reply_iter, variant_outer_iter; + + dbus->message_iter_init(reply, &reply_iter); + // The response has signature <> + if (dbus->message_iter_get_arg_type(&reply_iter) != DBUS_TYPE_VARIANT) + goto incorrect_type; + + dbus->message_iter_recurse(&reply_iter, &variant_outer_iter); + if (!DBus_ExtractPref(&variant_outer_iter, prop->setting)) + goto incorrect_type; + +incorrect_type: + dbus->message_unref(reply); + } + } + dbus->message_unref(msg); + } + + char buffer[2048]; + int written = 0; + written += SDL_snprintf(buffer, sizeof(buffer), "type='signal', interface='%s'," + "member='%s'", prop->signal_interface, + prop->signal_name); + + for (int j = 0; j < sizeof(prop->signal_args) / sizeof(*prop->signal_args) && prop->signal_args[j]; j++) { + if (written >= sizeof(buffer)) { + SDL_SetError("Binding system prefereces DBus key: buffer too small, this is a bug"); + return false; + } + + written += SDL_snprintf(buffer + written, sizeof(buffer) - written, ", arg%d='%s'", j, prop->signal_args[j]); + } + + if (written >= sizeof(buffer)) { + SDL_SetError("Binding system prefereces DBus key: buffer too small, this is a bug"); + return false; + } + + dbus->bus_add_match(dbus->session_conn, buffer, NULL); + } + + dbus->connection_add_filter(dbus->session_conn, + &DBus_MessageFilter, dbus, NULL); + dbus->connection_flush(dbus->session_conn); + + is_init = true; + + return true; +} + +bool Unix_GetSystemPreference(SDL_SystemPreference preference) +{ + switch (preference) + { + case SDL_SYSTEM_PREFERENCE_REDUCED_MOTION: + return !system_pref_data.animations; + + case SDL_SYSTEM_PREFERENCE_HIGH_CONTRAST: + return !!system_pref_data.contrast; + + case SDL_SYSTEM_PREFERENCE_COLORBLIND: + return !!system_pref_data.shapes; + + case SDL_SYSTEM_PREFERENCE_PERSIST_SCROLLBARS: + return !system_pref_data.hide_scrollbars; + + case SDL_SYSTEM_PREFERENCE_SCREEN_READER: + return !!system_pref_data.screen_reader; + + default: + return SDL_Unsupported(); + } +} + +bool Unix_GetSystemAccentColor(SDL_Color *color) +{ + return SDL_Unsupported(); +} + +float Unix_GetSystemTextScale(void) +{ + return (float) system_pref_data.text_scale; +} + +float Unix_GetSystemCursorScale(void) +{ + return (float) system_pref_data.cursor_size / 24.0f; +} diff --git a/src/core/linux/SDL_system_preferences.h b/src/core/linux/SDL_system_preferences.h new file mode 100644 index 0000000000000..e8018baee2893 --- /dev/null +++ b/src/core/linux/SDL_system_preferences.h @@ -0,0 +1,30 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2024 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#include "SDL_dbus.h" + +extern bool Unix_SystemPref_Init(void); + +extern bool Unix_GetSystemPreference(SDL_SystemPreference preference); +extern bool Unix_GetSystemAccentColor(SDL_Color *color); +extern float Unix_GetSystemTextScale(void); +extern float Unix_GetSystemCursorScale(void); diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 73f34843d887b..61543c2a5e4a6 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1234,6 +1234,10 @@ SDL3_0.0.0 { SDL_ClickTrayEntry; SDL_UpdateTrays; SDL_StretchSurface; + SDL_GetSystemPreference; + SDL_GetSystemAccentColor; + SDL_GetSystemTextScale; + SDL_GetSystemCursorScale; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index 77fd553c471f0..a4ba5db6a7ea9 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1259,3 +1259,7 @@ #define SDL_ClickTrayEntry SDL_ClickTrayEntry_REAL #define SDL_UpdateTrays SDL_UpdateTrays_REAL #define SDL_StretchSurface SDL_StretchSurface_REAL +#define SDL_GetSystemPreference SDL_GetSystemPreference_REAL +#define SDL_GetSystemAccentColor SDL_GetSystemAccentColor_REAL +#define SDL_GetSystemTextScale SDL_GetSystemTextScale_REAL +#define SDL_GetSystemCursorScale SDL_GetSystemCursorScale_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index e86ac2a325118..321b23967d04c 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1267,3 +1267,7 @@ SDL_DYNAPI_PROC(bool,SDL_AudioStreamDevicePaused,(SDL_AudioStream *a),(a),return SDL_DYNAPI_PROC(void,SDL_ClickTrayEntry,(SDL_TrayEntry *a),(a),) SDL_DYNAPI_PROC(void,SDL_UpdateTrays,(void),(),) SDL_DYNAPI_PROC(bool,SDL_StretchSurface,(SDL_Surface *a,const SDL_Rect *b,SDL_Surface *c,const SDL_Rect *d,SDL_ScaleMode e),(a,b,c,d,e),return) +SDL_DYNAPI_PROC(bool,SDL_GetSystemPreference,(SDL_SystemPreference a),(a),return) +SDL_DYNAPI_PROC(bool,SDL_GetSystemAccentColor,(SDL_Color *a),(a),return) +SDL_DYNAPI_PROC(float,SDL_GetSystemTextScale,(void),(),return) +SDL_DYNAPI_PROC(float,SDL_GetSystemCursorScale,(void),(),return) diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c index 7404234380e01..e810bfcaeeb2a 100644 --- a/src/events/SDL_events.c +++ b/src/events/SDL_events.c @@ -1968,6 +1968,18 @@ void SDL_SendSystemThemeChangedEvent(void) SDL_SendAppEvent(SDL_EVENT_SYSTEM_THEME_CHANGED); } +void SDL_SendSystemPreferenceChangedEvent(SDL_SystemPreference preference) +{ + if (SDL_EventEnabled(SDL_EVENT_SYSTEM_PREFERENCE_CHANGED)) { + SDL_Event event; + event.type = SDL_EVENT_SYSTEM_PREFERENCE_CHANGED; + event.common.timestamp = 0; + event.pref.pref = preference; + + SDL_PushEvent(&event); + } +} + bool SDL_InitEvents(void) { #ifdef SDL_PLATFORM_ANDROID diff --git a/src/events/SDL_events_c.h b/src/events/SDL_events_c.h index e56ac475e5a20..992301f3802e4 100644 --- a/src/events/SDL_events_c.h +++ b/src/events/SDL_events_c.h @@ -45,6 +45,7 @@ extern void SDL_SendAppEvent(SDL_EventType eventType); extern void SDL_SendKeymapChangedEvent(void); extern void SDL_SendLocaleChangedEvent(void); extern void SDL_SendSystemThemeChangedEvent(void); +extern void SDL_SendSystemPreferenceChangedEvent(SDL_SystemPreference preference); extern void *SDL_AllocateTemporaryMemory(size_t size); extern const char *SDL_CreateTemporaryString(const char *string); diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h index e043305aa06b8..ff1a6c118fe9c 100644 --- a/src/video/SDL_sysvideo.h +++ b/src/video/SDL_sysvideo.h @@ -388,6 +388,12 @@ struct SDL_VideoDevice // Display the system-level window menu void (*ShowWindowSystemMenu)(SDL_Window *window, int x, int y); + // System preferences + bool (*GetSystemPreference)(SDL_SystemPreference preference); + bool (*GetSystemAccentColor)(SDL_Color *color); + float (*GetSystemTextScale)(void); + float (*GetSystemCursorScale)(void); + /* * * */ // Data common to all drivers SDL_ThreadID thread; diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index e773969183f01..41e06eae19ea3 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -778,6 +778,42 @@ SDL_SystemTheme SDL_GetSystemTheme(void) } } +bool SDL_GetSystemPreference(SDL_SystemPreference preference) +{ + if (_this) { + return _this->GetSystemPreference(preference); + } else { + return false; + } +} + +bool SDL_GetSystemAccentColor(SDL_Color *color) +{ + if (_this) { + return _this->GetSystemAccentColor(color); + } else { + return false; + } +} + +float SDL_GetSystemTextScale(void) +{ + if (_this) { + return _this->GetSystemTextScale(); + } else { + return 1.0f; + } +} + +float SDL_GetSystemCursorScale(void) +{ + if (_this) { + return _this->GetSystemCursorScale(); + } else { + return 1.0f; + } +} + void SDL_UpdateDesktopBounds(void) { SDL_Rect rect; diff --git a/src/video/cocoa/SDL_cocoavideo.m b/src/video/cocoa/SDL_cocoavideo.m index aae54ebad8d96..22436a6a9c8f8 100644 --- a/src/video/cocoa/SDL_cocoavideo.m +++ b/src/video/cocoa/SDL_cocoavideo.m @@ -57,6 +57,12 @@ static void Cocoa_DeleteDevice(SDL_VideoDevice *device) } } +// Forward declarations for the video driver +static bool Cocoa_GetSystemPreference(SDL_SystemPreference preference); +static bool Cocoa_GetSystemAccentColor(SDL_Color *color); +static float Cocoa_GetSystemTextScale(void); +static float Cocoa_GetSystemCursorScale(void); + static SDL_VideoDevice *Cocoa_CreateDevice(void) { @autoreleasepool { @@ -188,6 +194,12 @@ static void Cocoa_DeleteDevice(SDL_VideoDevice *device) device->device_caps = VIDEO_DEVICE_CAPS_HAS_POPUP_WINDOW_SUPPORT | VIDEO_DEVICE_CAPS_SENDS_FULLSCREEN_DIMENSIONS; + + device->GetSystemPreference = Cocoa_GetSystemPreference; + device->GetSystemAccentColor = Cocoa_GetSystemAccentColor; + device->GetSystemTextScale = Cocoa_GetSystemTextScale; + device->GetSystemCursorScale = Cocoa_GetSystemCursorScale; + return device; } } @@ -256,6 +268,72 @@ SDL_SystemTheme Cocoa_GetSystemTheme(void) return SDL_SYSTEM_THEME_LIGHT; } +static bool Cocoa_GetSystemPreference(SDL_SystemPreference preference) +{ + switch(preference) + { + case SDL_SYSTEM_PREFERENCE_REDUCED_MOTION: + return [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceMotion]; + + case SDL_SYSTEM_PREFERENCE_REDUCED_TRANSPARENCY: + return [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceTransparency]; + + case SDL_SYSTEM_PREFERENCE_HIGH_CONTRAST: + return [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldIncreaseContrast]; + + case SDL_SYSTEM_PREFERENCE_COLORBLIND: + return [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldDifferentiateWithoutColor]; + + case SDL_SYSTEM_PREFERENCE_SCREEN_READER: + return [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]; + + // FIXME: This doesn't work on my Mac, and I can't find any other way to do it. + // case SDL_SYSTEM_PREFERENCE_PERSIST_SCROLLBARS: + // return [[NSUserDefaults standardUserDefaults] integerForKey:@"AppleShowScrollBars"] == 1; + + default: + return SDL_Unsupported(); + } +} + +// https://stackoverflow.com/questions/58543327/detecting-when-macos-10-14-accent-color-has-changed +static bool Cocoa_GetSystemAccentColor(SDL_Color *color) +{ + @autoreleasepool { + if (@available(macOS 10.14, *)) { + if (!color) { + return SDL_InvalidParamError("color"); + } + + NSColor *accent = [NSColor controlAccentColor]; + + NSColor *rgbAccent = [accent colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + + CGFloat r, g, b, a; + [rgbAccent getRed:&r green:&g blue:&b alpha:&a]; + + color->r = (Uint8) (255.0f * r); + color->g = (Uint8) (255.0f * g); + color->b = (Uint8) (255.0f * b); + color->a = (Uint8) (255.0f * a); + + return true; + } else { + return false; + } + } +} + +static float Cocoa_GetSystemTextScale(void) +{ + return 1.0f; +} + +static float Cocoa_GetSystemCursorScale(void) +{ + return 1.0f; +} + // This function assumes that it's called from within an autorelease pool NSImage *Cocoa_CreateImage(SDL_Surface *surface) { diff --git a/src/video/emscripten/SDL_emscriptenvideo.c b/src/video/emscripten/SDL_emscriptenvideo.c index 413d96fc6089a..27f8fcea53b78 100644 --- a/src/video/emscripten/SDL_emscriptenvideo.c +++ b/src/video/emscripten/SDL_emscriptenvideo.c @@ -92,7 +92,7 @@ static SDL_SystemTheme Emscripten_GetSystemTheme(void) } } -static void Emscripten_ListenSystemTheme(void) +static void Emscripten_ListenEvents(void) { MAIN_THREAD_EM_ASM({ if (window.matchMedia) { @@ -108,19 +108,60 @@ static void Emscripten_ListenSystemTheme(void) SDL3.themeChangedMatchMedia = window.matchMedia('(prefers-color-scheme: dark)'); SDL3.themeChangedMatchMedia.addEventListener('change', SDL3.eventHandlerThemeChanged); + + SDL3.eventHandlerPrefMotionChanged = function(event) { + _Emscripten_SendSystemPrefMotionChangedEvent(); + }; + + SDL3.prefMotionChangedMatchMedia = window.matchMedia('(prefers-reduced-motion)'); + SDL3.prefMotionChangedMatchMedia.addEventListener('change', SDL3.eventHandlerPrefMotionChanged); + + SDL3.eventHandlerPrefTransparencyChanged = function(event) { + _Emscripten_SendSystemPrefTransparencyChangedEvent(); + }; + + SDL3.prefTransparencyChangedMatchMedia = window.matchMedia('(prefers-reduced-transparency)'); + SDL3.prefTransparencyChangedMatchMedia.addEventListener('change', SDL3.eventHandlerPrefTransparencyChanged); + + SDL3.eventHandlerPrefContrastChanged = function(event) { + _Emscripten_SendSystemPrefContrastChangedEvent(); + }; + + SDL3.prefContrastChangedMatchMedia = window.matchMedia('(prefers-contrast)'); + SDL3.prefContrastChangedMatchMedia.addEventListener('change', SDL3.eventHandlerPrefContrastChanged); } }); } -static void Emscripten_UnlistenSystemTheme(void) +static void Emscripten_UnlistenEvents(void) { MAIN_THREAD_EM_ASM({ if (typeof(Module['SDL3']) !== 'undefined') { var SDL3 = Module['SDL3']; - SDL3.themeChangedMatchMedia.removeEventListener('change', SDL3.eventHandlerThemeChanged); - SDL3.themeChangedMatchMedia = undefined; - SDL3.eventHandlerThemeChanged = undefined; + if (SDL3.themeChangedMatchMedia) { + SDL3.themeChangedMatchMedia.removeEventListener('change', SDL3.eventHandlerThemeChanged); + SDL3.themeChangedMatchMedia = undefined; + SDL3.eventHandlerThemeChanged = undefined; + } + + if (SDL3.prefMotionChangedMatchMedia) { + SDL3.prefMotionChangedMatchMedia.removeEventListener('change', SDL3.eventHandlerPrefMotionChanged); + SDL3.prefMotionChangedMatchMedia = undefined; + SDL3.eventHandlerPrefMotionChanged = undefined; + } + + if (SDL3.prefTransparencyChangedMatchMedia) { + SDL3.prefTransparencyChangedMatchMedia.removeEventListener('change', SDL3.eventHandlerPrefTransparencyChanged); + SDL3.prefTransparencyChangedMatchMedia = undefined; + SDL3.eventHandlerPrefTransparencyChanged = undefined; + } + + if (SDL3.prefContrastChangedMatchMedia) { + SDL3.prefContrastChangedMatchMedia.removeEventListener('change', SDL3.eventHandlerPrefContrastChanged); + SDL3.prefContrastChangedMatchMedia = undefined; + SDL3.eventHandlerPrefContrastChanged = undefined; + } } }); } @@ -130,6 +171,138 @@ EMSCRIPTEN_KEEPALIVE void Emscripten_SendSystemThemeChangedEvent(void) SDL_SetSystemTheme(Emscripten_GetSystemTheme()); } +static bool Emscripten_GetSystemPreference(SDL_SystemPreference preference) +{ + int enabled; + + switch (preference) { + +#define MATCH(pref, css) \ + case pref: \ + enabled = EM_ASM_INT({ \ + if (!window.matchMedia) { \ + return -1; \ + } \ + return window.matchMedia(css).matches ? 1 : 0; \ + }); \ + return enabled; + +MATCH(SDL_SYSTEM_PREFERENCE_REDUCED_MOTION, '(prefers-reduced-motion)') +MATCH(SDL_SYSTEM_PREFERENCE_REDUCED_TRANSPARENCY, '(prefers-reduced-transparency)') +MATCH(SDL_SYSTEM_PREFERENCE_HIGH_CONTRAST, '(prefers-contrast: more)') + +#undef MATCH + + default: + return SDL_Unsupported(); + } +} + +EMSCRIPTEN_KEEPALIVE void Emscripten_SendSystemPrefMotionChangedEvent(void) +{ + SDL_SendSystemPreferenceChangedEvent(SDL_SYSTEM_PREFERENCE_REDUCED_MOTION); +} + +EMSCRIPTEN_KEEPALIVE void Emscripten_SendSystemPrefTransparencyChangedEvent(void) +{ + SDL_SendSystemPreferenceChangedEvent(SDL_SYSTEM_PREFERENCE_REDUCED_TRANSPARENCY); +} + +EMSCRIPTEN_KEEPALIVE void Emscripten_SendSystemPrefContrastChangedEvent(void) +{ + SDL_SendSystemPreferenceChangedEvent(SDL_SYSTEM_PREFERENCE_HIGH_CONTRAST); +} + +// As of writing this (Jan 5, 2025), this works in latest Firefox and Safari, +// and is implemented but disabled by default in Chromium-based browsers. Info: +// https://developer.mozilla.org/en-US/docs/Web/CSS/system-color#accentcolor +static bool Emscripten_GetSystemAccentColor(SDL_Color *color) +{ + if (!color) { + SDL_InvalidParamError("color"); + } + + // TODO: Static assert that sizeof(int) >= 4? + int color_info = EM_ASM_INT({ + const a = document.createElement('div'); + a.style.display = 'none'; + a.style.color = 'AccentColor'; + + // It's less likely to mess with pages in the head (it needs to be in + // the document somewhere, else it won't work) + document.head.appendChild(a); + + // Note: It can be any valid CSS color identifier, including non-rgb + // values. In practice, this should be rare, but it's not impossible. + // https://stackoverflow.com/questions/67005331/is-color-format-specified-in-the-spec-for-getcomputedstyle#answer-79292386 + const color = window.getComputedStyle(a).color; + a.remove(); + + // Parse only rgb(a); too bad if it's in a different format. + const rgbMatch = color.match(/^rgba?\\(([0-9]+),? ([0-9]+),? ([0-9]+)(,? ([0-9]+))?\\)$/); + + if (!rgbMatch) { + return 0; + } + + let c = 0; + let n; + // Match format: ['rgba(1, 2, 3, 4)', '1', '2', '3', ', 4', '4'] + // or: ['rgb(1, 2, 3)', '1', '2', '3', undefined, undefined] + if (rgbMatch[5]) { + n = parseInt(rgbMatch[5]); + if (n < 0) n = 0; + if (n > 255) n = 255; + c += n; + } + + c *= 255; + n = parseInt(rgbMatch[3]); + if (n < 0) n = 0; + if (n > 255) n = 255; + c += n; + + c *= 255; + n = parseInt(rgbMatch[2]); + if (n < 0) n = 0; + if (n > 255) n = 255; + c += n; + + c *= 255; + n = parseInt(rgbMatch[1]); + if (n < 0) n = 0; + if (n > 255) n = 255; + c += n; + + return c; + }); + + if (color == 0) { + return false; + } + + color->r = color_info % 255; + color->g = (color_info / 255) % 255; + color->b = (color_info / 65535) % 255; + color->a = (color_info / 16777216) % 255; + + return true; +} + +static float Emscripten_GetSystemTextScale(void) +{ + double scaling = EM_ASM_DOUBLE({ + return parseFloat(window.getComputedStyle(document.documentElement).fontSize) / 16.0; + }); + + return (float) scaling; +} + +static float Emscripten_GetSystemCursorScale(void) +{ + return 1.0f; +} + static SDL_VideoDevice *Emscripten_CreateDevice(void) { SDL_VideoDevice *device; @@ -184,9 +357,14 @@ static SDL_VideoDevice *Emscripten_CreateDevice(void) device->GL_SwapWindow = Emscripten_GLES_SwapWindow; device->GL_DestroyContext = Emscripten_GLES_DestroyContext; + device->GetSystemPreference = Emscripten_GetSystemPreference; + device->GetSystemAccentColor = Emscripten_GetSystemAccentColor; + device->GetSystemTextScale = Emscripten_GetSystemTextScale; + device->GetSystemCursorScale = Emscripten_GetSystemCursorScale; + device->free = Emscripten_DeleteDevice; - Emscripten_ListenSystemTheme(); + Emscripten_ListenEvents(); device->system_theme = Emscripten_GetSystemTheme(); return device; @@ -231,7 +409,7 @@ static bool Emscripten_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay * static void Emscripten_VideoQuit(SDL_VideoDevice *_this) { Emscripten_QuitMouse(); - Emscripten_UnlistenSystemTheme(); + Emscripten_UnlistenEvents(); pumpevents_has_run = false; pending_swap_interval = -1; } diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index fc8edc1e025a5..c56a8ea9186aa 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -24,6 +24,7 @@ #ifdef SDL_VIDEO_DRIVER_WAYLAND #include "../../core/linux/SDL_system_theme.h" +#include "../../core/linux/SDL_system_preferences.h" #include "../../events/SDL_events_c.h" #include "SDL_waylandclipboard.h" @@ -641,6 +642,13 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols) #ifdef SDL_USE_LIBDBUS if (SDL_SystemTheme_Init()) device->system_theme = SDL_SystemTheme_Get(); + + if (Unix_SystemPref_Init()) { + device->GetSystemPreference = Unix_GetSystemPreference; + device->GetSystemAccentColor = Unix_GetSystemAccentColor; + device->GetSystemTextScale = Unix_GetSystemTextScale; + device->GetSystemCursorScale = Unix_GetSystemCursorScale; + } #endif device->GetTextMimeTypes = Wayland_GetTextMimeTypes; diff --git a/src/video/windows/SDL_windowsvideo.c b/src/video/windows/SDL_windowsvideo.c index ff7c0c2a2d291..a43c76db7b0bc 100644 --- a/src/video/windows/SDL_windowsvideo.c +++ b/src/video/windows/SDL_windowsvideo.c @@ -89,6 +89,162 @@ static bool WIN_SuspendScreenSaver(SDL_VideoDevice *_this) extern void D3D12_XBOX_GetResolution(Uint32 *width, Uint32 *height); #endif +static bool WIN_GetSystemPreference(SDL_SystemPreference preference) +{ + switch(preference) + { + case SDL_SYSTEM_PREFERENCE_REDUCED_MOTION: + { + BOOL option = false; + + if (!SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &option, 0)) { + return WIN_SetError("Could not invoke SystemParametersInfoW with SDL_SYSTEM_PREFERENCE_REDUCED_MOTION"); + } + + return !option; + } + + case SDL_SYSTEM_PREFERENCE_REDUCED_TRANSPARENCY: + { + HKEY hKey; + if (RegOpenKeyEx(HKEY_CURRENT_USER, + TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"), + 0, + KEY_READ, + &hKey) == ERROR_SUCCESS) { + DWORD enableTransparency; + DWORD size = sizeof(enableTransparency); + if (RegQueryValueEx(hKey, TEXT("EnableTransparency"), NULL, NULL, (LPBYTE)&enableTransparency, &size) == ERROR_SUCCESS) { + RegCloseKey(hKey); + return !enableTransparency; + } + RegCloseKey(hKey); + } + + return false; + } + + case SDL_SYSTEM_PREFERENCE_HIGH_CONTRAST: + { + HIGHCONTRAST high_contrast_data; + high_contrast_data.cbSize = sizeof(HIGHCONTRAST); + + if (!SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(HIGHCONTRAST), &high_contrast_data, 0)) { + return WIN_SetError("Could not invoke SystemParametersInfoW with SDL_SYSTEM_PREFERENCE_HIGH_CONTRAST"); + } + + return high_contrast_data.dwFlags & HCF_HIGHCONTRASTON; + } + + case SDL_SYSTEM_PREFERENCE_PERSIST_SCROLLBARS: + { + HKEY hKey; + if (RegOpenKeyEx(HKEY_CURRENT_USER, + TEXT("Control Panel\\Accessibility"), + 0, + KEY_READ, + &hKey) == ERROR_SUCCESS) { + DWORD autoHideScrollBars; + DWORD size = sizeof(autoHideScrollBars); + if (RegQueryValueEx(hKey, TEXT("DynamicScrollbars"), NULL, NULL, (LPBYTE)&autoHideScrollBars, &size) == ERROR_SUCCESS) { + RegCloseKey(hKey); + return !autoHideScrollBars; + } + RegCloseKey(hKey); + } + + return false; + } + + case SDL_SYSTEM_PREFERENCE_SCREEN_READER: + { + BOOL option = false; + + // FIXME: Documentation states that this won't work if the screen + // reader is Windows' built-in "Narrator" program + if (!SystemParametersInfoW(SPI_GETSCREENREADER, 0, &option, 0)) { + return WIN_SetError("Could not invoke SystemParametersInfoW with SPI_GETSCREENREADER"); + } + + return option; + } + + default: + return SDL_Unsupported(); + } +} + +static bool WIN_GetSystemAccentColor(SDL_Color *color) +{ + if (!color) { + return SDL_InvalidParamError("color"); + } + + HKEY hKey; + if (RegOpenKeyEx(HKEY_CURRENT_USER, + TEXT("SOFTWARE\\Microsoft\\Windows\\DWM"), + 0, + KEY_READ, + &hKey) == ERROR_SUCCESS) { + DWORD colordata; + DWORD size = sizeof(colordata); + if (RegQueryValueEx(hKey, TEXT("AccentColor"), NULL, NULL, (LPBYTE)&colordata, &size) == ERROR_SUCCESS) { + RegCloseKey(hKey); + + // TODO: Check endianness? + color->r = colordata & 0x000000ff; + color->g = (colordata & 0x0000ff00) >> 8; + color->b = (colordata & 0x00ff0000) >> 16; + color->a = (colordata & 0xff000000) >> 24; + + return true; + } + RegCloseKey(hKey); + } + + return SDL_SetError("Could not fetch accent color registry key"); +} + +static float WIN_GetSystemTextScale(void) +{ + HKEY hKey; + if (RegOpenKeyEx(HKEY_CURRENT_USER, + TEXT("SOFTWARE\\Microsoft\\Accessibility"), + 0, + KEY_READ, + &hKey) == ERROR_SUCCESS) { + DWORD text; + DWORD size = sizeof(text); + if (RegQueryValueEx(hKey, TEXT("TestScaleFactor"), NULL, NULL, (LPBYTE)&text, &size) == ERROR_SUCCESS) { + RegCloseKey(hKey); + return ((float) text) / 100.0f; + } + RegCloseKey(hKey); + } + + return 1.0f; +} + +static float WIN_GetSystemCursorScale(void) +{ + HKEY hKey; + if (RegOpenKeyEx(HKEY_CURRENT_USER, + TEXT("Control Panel\\Cursors"), + 0, + KEY_READ, + &hKey) == ERROR_SUCCESS) { + DWORD cursor; + DWORD size = sizeof(cursor); + if (RegQueryValueEx(hKey, TEXT("CursorBaseSize"), NULL, NULL, (LPBYTE)&cursor, &size) == ERROR_SUCCESS) { + RegCloseKey(hKey); + return ((float) cursor) / 32.0f; + } + RegCloseKey(hKey); + } + + return 1.0f; +} + // Windows driver bootstrap functions static void WIN_DeleteDevice(SDL_VideoDevice *device) @@ -324,6 +480,11 @@ static SDL_VideoDevice *WIN_CreateDevice(void) device->device_caps = VIDEO_DEVICE_CAPS_HAS_POPUP_WINDOW_SUPPORT | VIDEO_DEVICE_CAPS_SENDS_FULLSCREEN_DIMENSIONS; + device->GetSystemPreference = WIN_GetSystemPreference; + device->GetSystemAccentColor = WIN_GetSystemAccentColor; + device->GetSystemTextScale = WIN_GetSystemTextScale; + device->GetSystemCursorScale = WIN_GetSystemCursorScale; + return device; } diff --git a/src/video/x11/SDL_x11video.c b/src/video/x11/SDL_x11video.c index d87dcc0f10ed6..11ba1e7ce5011 100644 --- a/src/video/x11/SDL_x11video.c +++ b/src/video/x11/SDL_x11video.c @@ -25,6 +25,7 @@ #include // For getpid() and readlink() #include "../../core/linux/SDL_system_theme.h" +#include "../../core/linux/SDL_system_preferences.h" #include "../../events/SDL_keyboard_c.h" #include "../../events/SDL_mouse_c.h" #include "../SDL_pixels_c.h" @@ -254,6 +255,13 @@ static SDL_VideoDevice *X11_CreateDevice(void) #ifdef SDL_USE_LIBDBUS if (SDL_SystemTheme_Init()) device->system_theme = SDL_SystemTheme_Get(); + + if (Unix_SystemPref_Init()) { + device->GetSystemPreference = Unix_GetSystemPreference; + device->GetSystemAccentColor = Unix_GetSystemAccentColor; + device->GetSystemTextScale = Unix_GetSystemTextScale; + device->GetSystemCursorScale = Unix_GetSystemCursorScale; + } #endif device->device_caps = VIDEO_DEVICE_CAPS_HAS_POPUP_WINDOW_SUPPORT | diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3be04e215e5ee..c680745c1e561 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -412,6 +412,7 @@ add_sdl_test_executable(testtime SOURCES testtime.c) add_sdl_test_executable(testmanymouse SOURCES testmanymouse.c) add_sdl_test_executable(testmodal SOURCES testmodal.c) add_sdl_test_executable(testtray SOURCES testtray.c) +add_sdl_test_executable(testsyspref SOURCES testsyspref.c) add_sdl_test_executable(testprocess diff --git a/test/testsyspref.c b/test/testsyspref.c new file mode 100644 index 0000000000000..cef9e6735794d --- /dev/null +++ b/test/testsyspref.c @@ -0,0 +1,88 @@ +#include +#include +#include + +int main(int argc, char *argv[]) +{ + SDL_Color color; + SDL_Event e; + SDLTest_CommonState *state; + int i; + + state = SDLTest_CommonCreateState(argv, 0); + if (state == NULL) { + return 1; + } + + /* Parse commandline */ + for (i = 1; i < argc;) { + int consumed; + + consumed = SDLTest_CommonArg(state, i); + + if (consumed <= 0) { + static const char *options[] = { NULL }; + SDLTest_CommonLogUsage(state, argv[0], options); + return 1; + } + + i += consumed; + } + + if (!SDL_Init(SDL_INIT_VIDEO)) { + SDL_Log("SDL_Init failed (%s)", SDL_GetError()); + return 1; + } + +#define LOG(val) SDL_Log(#val ": %d\n", SDL_GetSystemPreference(val)) + LOG(SDL_SYSTEM_PREFERENCE_REDUCED_MOTION); + LOG(SDL_SYSTEM_PREFERENCE_REDUCED_TRANSPARENCY); + LOG(SDL_SYSTEM_PREFERENCE_HIGH_CONTRAST); + LOG(SDL_SYSTEM_PREFERENCE_COLORBLIND); + LOG(SDL_SYSTEM_PREFERENCE_PERSIST_SCROLLBARS); + LOG(SDL_SYSTEM_PREFERENCE_SCREEN_READER); +#undef LOG + + SDL_Log("Text scale: %f\n", SDL_GetSystemTextScale()); + SDL_Log("Cursor scale: %f\n", SDL_GetSystemCursorScale()); + + if (SDL_GetSystemAccentColor(&color)) { + SDL_Log("Accent color: %d %d %d %d\n", color.r, color.g, color.b, color.a); + } else { + SDL_Log("Could not get accent color: %s\n", SDL_GetError()); + } + + while (SDL_WaitEvent(&e)) { + if (e.type == SDL_EVENT_QUIT) { + break; + } else if (e.type == SDL_EVENT_SYSTEM_PREFERENCE_CHANGED) { + switch (e.pref.pref) { +#define CHECK(val) case val: SDL_Log(#val " updated: %d\n", SDL_GetSystemPreference(val)); break; + CHECK(SDL_SYSTEM_PREFERENCE_REDUCED_MOTION); + CHECK(SDL_SYSTEM_PREFERENCE_REDUCED_TRANSPARENCY); + CHECK(SDL_SYSTEM_PREFERENCE_HIGH_CONTRAST); + CHECK(SDL_SYSTEM_PREFERENCE_COLORBLIND); + CHECK(SDL_SYSTEM_PREFERENCE_PERSIST_SCROLLBARS); + CHECK(SDL_SYSTEM_PREFERENCE_SCREEN_READER); +#undef CHECK + default: + SDL_Log("Unknown value '%d' updated: %d\n", e.pref.pref, SDL_GetSystemPreference(e.pref.pref)); + } + } else if (e.type == SDL_EVENT_SYSTEM_TEXT_SCALE_CHANGED) { + SDL_Log("Text scaling updated: %f\n", SDL_GetSystemTextScale()); + } else if (e.type == SDL_EVENT_SYSTEM_CURSOR_SCALE_CHANGED) { + SDL_Log("Cursor scaling updated: %f\n", SDL_GetSystemCursorScale()); + } else if (e.type == SDL_EVENT_SYSTEM_ACCENT_COLOR_CHANGED) { + if (SDL_GetSystemAccentColor(&color)) { + SDL_Log("Accent color updated: %d %d %d %d\n", color.r, color.g, color.b, color.a); + } else { + SDL_Log("Accent color updated, could not get accent color: %s\n", SDL_GetError()); + } + } + } + + SDL_Quit(); + SDLTest_CommonDestroyState(state); + + return 0; +}