From ee53ae28dff4ca227ba970c733bf89d53f432141 Mon Sep 17 00:00:00 2001 From: bruvzg <7645683+bruvzg@users.noreply.github.com> Date: Fri, 19 Jan 2024 20:46:26 +0200 Subject: [PATCH] Add method to get "base" system UI color (macOS/Windows) and system theme change callback. --- doc/classes/DisplayServer.xml | 19 ++++- platform/android/display_server_android.cpp | 10 +++ platform/android/display_server_android.h | 5 ++ .../lib/src/org/godotengine/godot/Godot.kt | 21 ++++-- .../org/godotengine/godot/GodotFragment.java | 8 +++ .../src/org/godotengine/godot/GodotLib.java | 5 ++ platform/android/java_godot_lib_jni.cpp | 7 ++ platform/android/java_godot_lib_jni.h | 1 + platform/ios/display_server_ios.h | 8 ++- platform/ios/display_server_ios.mm | 10 +++ platform/ios/godot_view.mm | 17 +++++ .../linuxbsd/freedesktop_portal_desktop.cpp | 72 +++++++++++++++++-- .../linuxbsd/freedesktop_portal_desktop.h | 15 +++- .../wayland/display_server_wayland.cpp | 4 ++ .../linuxbsd/wayland/display_server_wayland.h | 1 + platform/linuxbsd/x11/display_server_x11.cpp | 4 ++ platform/linuxbsd/x11/display_server_x11.h | 1 + platform/macos/display_server_macos.h | 6 ++ platform/macos/display_server_macos.mm | 47 +++++++++++- platform/macos/godot_application_delegate.mm | 14 ++++ platform/windows/display_server_windows.cpp | 19 ++++- platform/windows/display_server_windows.h | 4 ++ servers/display_server.cpp | 2 + servers/display_server.h | 4 +- 24 files changed, 282 insertions(+), 22 deletions(-) diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 09bb20af79d..36658c6e9f7 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -136,7 +136,7 @@ Displays OS native dialog for selecting files or directories in the file system. Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int[/code]. - [b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG] feature. Supported platforms include Linux (X11 and Wayland), Windows, and macOS. + [b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG] feature. Supported platforms include Linux (X11/Wayland), Windows, and macOS. [b]Note:[/b] [param current_directory] might be ignored. [b]Note:[/b] On Linux, [param show_hidden] is ignored. [b]Note:[/b] On macOS, native file dialogs have no title. @@ -161,7 +161,7 @@ - [code]"values"[/code] - [PackedStringArray] of values. If empty, boolean option (check box) is used. - [code]"default"[/code] - default selected option index ([int]) or default boolean value ([bool]). Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int, selected_option: Dictionary[/code]. - [b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG] feature. Supported platforms include Linux (X11 and Wayland), Windows, and macOS. + [b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG] feature. Supported platforms include Linux (X11/Wayland), Windows, and macOS. [b]Note:[/b] [param current_directory] might be ignored. [b]Note:[/b] On Linux (X11), [param show_hidden] is ignored. [b]Note:[/b] On macOS, native file dialogs have no title. @@ -182,6 +182,13 @@ [b]Note:[/b] This method is implemented on macOS and Windows. + + + + Returns the OS theme base color (default control background). Returns [code]Color(0, 0, 0, 0)[/code] if the base color is unknown. + [b]Note:[/b] This method is implemented on macOS and Windows. + + @@ -1136,6 +1143,14 @@ Sets the window icon (usually displayed in the top-left corner) in the operating system's [i]native[/i] format. The file at [param filename] must be in [code].ico[/code] format on Windows or [code].icns[/code] on macOS. By using specially crafted [code].ico[/code] or [code].icns[/code] icons, [method set_native_icon] allows specifying different icons depending on the size the icon is displayed at. This size is determined by the operating system and user preferences (including the display scale factor). To use icons in other formats, use [method set_icon] instead. + + + + + Sets the [param callable] that should be called when system theme settings are changed. Callback method should have zero arguments. + [b]Note:[/b] This method is implemented on Android, iOS, macOS, Windows, and Linux (X11/Wayland). + + diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index b06164246eb..01ecbc7164e 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -127,6 +127,16 @@ bool DisplayServerAndroid::is_dark_mode() const { return godot_java->is_dark_mode(); } +void DisplayServerAndroid::set_system_theme_change_callback(const Callable &p_callable) { + system_theme_changed = p_callable; +} + +void DisplayServerAndroid::emit_system_theme_changed() { + if (system_theme_changed.is_valid()) { + system_theme_changed.call_deferred(); + } +} + void DisplayServerAndroid::clipboard_set(const String &p_text) { GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); ERR_FAIL_NULL(godot_java); diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index b425b2d9ae5..c95eaddf93c 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -84,6 +84,8 @@ class DisplayServerAndroid : public DisplayServer { Callable input_text_callback; Callable rect_changed_callback; + Callable system_theme_changed; + void _window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred = false) const; static void _dispatch_input_events(const Ref &p_event); @@ -103,8 +105,11 @@ public: virtual void tts_resume() override; virtual void tts_stop() override; + void emit_system_theme_changed(); + virtual bool is_dark_mode_supported() const override; virtual bool is_dark_mode() const override; + virtual void set_system_theme_change_callback(const Callable &p_callable) override; virtual void clipboard_set(const String &p_text) override; virtual String clipboard_get() const override; diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index da86e67c7d3..dd007a42542 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -149,6 +149,7 @@ class Godot(private val context: Context) : SensorEventListener { private var useApkExpansion = false private var useImmersive = false private var useDebugOpengl = false + private var darkMode = false; private var containerLayout: FrameLayout? = null var renderView: GodotRenderView? = null @@ -184,6 +185,8 @@ class Godot(private val context: Context) : SensorEventListener { return } + darkMode = context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + beginBenchmarkMeasure("Startup", "Godot::onCreate") try { this.primaryHost = primaryHost @@ -559,6 +562,17 @@ class Godot(private val context: Context) : SensorEventListener { } } + /** + * Configuration change callback + */ + fun onConfigurationChanged(newConfig: Configuration) { + var newDarkMode = newConfig.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + if (darkMode != newDarkMode) { + darkMode = newDarkMode + GodotLib.onNightModeChanged() + } + } + /** * Activity result callback */ @@ -731,7 +745,7 @@ class Godot(private val context: Context) : SensorEventListener { */ @Keep private fun isDarkModeSupported(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + return context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_UNDEFINED } /** @@ -739,10 +753,7 @@ class Godot(private val context: Context) : SensorEventListener { */ @Keep private fun isDarkMode(): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES - } - return false + return darkMode } fun hasClipboard(): Boolean { diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java index 643c9a658e9..a323045e1b9 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java @@ -38,6 +38,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Messenger; @@ -145,6 +146,13 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH parentHost = null; } + @CallSuper + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + godot.onConfigurationChanged(newConfig); + } + @CallSuper @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index fee50e93c2e..d0c3d4a687e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -219,6 +219,11 @@ public class GodotLib { */ public static native void requestPermissionResult(String p_permission, boolean p_result); + /** + * Invoked on the theme light/dark mode change. + */ + public static native void onNightModeChanged(); + /** * Invoked on the GL thread to configure the height of the virtual keyboard. */ diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 08e792cc041..85d5cf27961 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -487,6 +487,13 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv * Callable(obj, str_method).call_deferredp(argptrs, count); } +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz) { + DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton(); + if (ds) { + ds->emit_system_theme_changed(); + } +} + JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result) { String permission = jstring_to_string(p_permission, env); if (permission == "android.permission.RECORD_AUDIO" && p_result) { diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 1ddda6c1c8c..f32ffc291a0 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -66,6 +66,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *en JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); } diff --git a/platform/ios/display_server_ios.h b/platform/ios/display_server_ios.h index 3efd2498d48..6f66783a475 100644 --- a/platform/ios/display_server_ios.h +++ b/platform/ios/display_server_ios.h @@ -77,6 +77,8 @@ class DisplayServerIOS : public DisplayServer { Callable input_event_callback; Callable input_text_callback; + Callable system_theme_changed; + int virtual_keyboard_height = 0; void perform_event(const Ref &p_event); @@ -109,6 +111,8 @@ public: void send_window_event(DisplayServer::WindowEvent p_event) const; void _window_callback(const Callable &p_callable, const Variant &p_arg) const; + void emit_system_theme_changed(); + // MARK: - Input // MARK: Touches and Apple Pencil @@ -145,6 +149,7 @@ public: virtual bool is_dark_mode_supported() const override; virtual bool is_dark_mode() const override; + virtual void set_system_theme_change_callback(const Callable &p_callable) override; virtual Rect2i get_display_safe_area() const override; @@ -159,8 +164,7 @@ public: virtual Vector get_window_list() const override; - virtual WindowID - get_window_at_screen_position(const Point2i &p_position) const override; + virtual WindowID get_window_at_screen_position(const Point2i &p_position) const override; virtual int64_t window_get_native_handle(HandleType p_handle_type, WindowID p_window = MAIN_WINDOW_ID) const override; diff --git a/platform/ios/display_server_ios.mm b/platform/ios/display_server_ios.mm index 2895dffdfae..ed69b91fdd0 100644 --- a/platform/ios/display_server_ios.mm +++ b/platform/ios/display_server_ios.mm @@ -387,6 +387,16 @@ bool DisplayServerIOS::is_dark_mode() const { } } +void DisplayServerIOS::set_system_theme_change_callback(const Callable &p_callable) { + system_theme_changed = p_callable; +} + +void DisplayServerIOS::emit_system_theme_changed() { + if (system_theme_changed.is_valid()) { + system_theme_changed.call(); + } +} + Rect2i DisplayServerIOS::get_display_safe_area() const { UIEdgeInsets insets = UIEdgeInsetsZero; UIView *view = AppDelegate.viewController.godotView; diff --git a/platform/ios/godot_view.mm b/platform/ios/godot_view.mm index ff8a4f89213..4b87863fc52 100644 --- a/platform/ios/godot_view.mm +++ b/platform/ios/godot_view.mm @@ -167,6 +167,23 @@ static const float earth_gravity = 9.80665; } } +- (void)system_theme_changed { + DisplayServerIOS *ds = (DisplayServerIOS *)DisplayServer::get_singleton(); + if (ds) { + ds->emit_system_theme_changed(); + } +} + +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { + if (@available(iOS 13.0, *)) { + [super traitCollectionDidChange:previousTraitCollection]; + + if ([UITraitCollection currentTraitCollection].userInterfaceStyle != previousTraitCollection.userInterfaceStyle) { + [self system_theme_changed]; + } + } +} + - (void)stopRendering { if (!self.isActive) { return; diff --git a/platform/linuxbsd/freedesktop_portal_desktop.cpp b/platform/linuxbsd/freedesktop_portal_desktop.cpp index a3633e72b7c..cdebed58b29 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.cpp +++ b/platform/linuxbsd/freedesktop_portal_desktop.cpp @@ -528,10 +528,10 @@ void FreeDesktopPortalDesktop::_file_dialog_callback(const Callable &p_callable, } } -void FreeDesktopPortalDesktop::_thread_file_dialog_monitor(void *p_ud) { +void FreeDesktopPortalDesktop::_thread_monitor(void *p_ud) { FreeDesktopPortalDesktop *portal = (FreeDesktopPortalDesktop *)p_ud; - while (!portal->file_dialog_thread_abort.is_set()) { + while (!portal->monitor_thread_abort.is_set()) { { MutexLock lock(portal->file_dialog_mutex); for (int i = portal->file_dialogs.size() - 1; i >= 0; i--) { @@ -579,10 +579,44 @@ void FreeDesktopPortalDesktop::_thread_file_dialog_monitor(void *p_ud) { } } } + + if (portal->theme_connection) { + while (true) { + DBusMessage *msg = dbus_connection_pop_message(portal->theme_connection); + if (!msg) { + break; + } else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Settings", "SettingChanged")) { + DBusMessageIter iter; + if (dbus_message_iter_init(msg, &iter)) { + const char *value; + dbus_message_iter_get_basic(&iter, &value); + String name_space = String::utf8(value); + dbus_message_iter_next(&iter); + dbus_message_iter_get_basic(&iter, &value); + String key = String::utf8(value); + + if (name_space == "org.freedesktop.appearance" && key == "color-scheme") { + callable_mp(portal, &FreeDesktopPortalDesktop::_system_theme_changed_callback).call_deferred(); + } + } + dbus_message_unref(msg); + break; + } + dbus_message_unref(msg); + } + dbus_connection_read_write(portal->theme_connection, 0); + } + usleep(50000); } } +void FreeDesktopPortalDesktop::_system_theme_changed_callback() { + if (system_theme_changed.is_valid()) { + system_theme_changed.call(); + } +} + FreeDesktopPortalDesktop::FreeDesktopPortalDesktop() { #ifdef SOWRAP_ENABLED #ifdef DEBUG_ENABLED @@ -611,17 +645,34 @@ FreeDesktopPortalDesktop::FreeDesktopPortalDesktop() { unsupported = true; } + DBusError err; + dbus_error_init(&err); + theme_connection = dbus_bus_get(DBUS_BUS_SESSION, &err); + if (dbus_error_is_set(&err)) { + dbus_error_free(&err); + } else { + theme_path = "type='signal',sender='org.freedesktop.portal.Desktop',interface='org.freedesktop.portal.Settings',member='SettingChanged'"; + dbus_bus_add_match(theme_connection, theme_path.utf8().get_data(), &err); + if (dbus_error_is_set(&err)) { + dbus_error_free(&err); + dbus_connection_unref(theme_connection); + theme_connection = nullptr; + } + dbus_connection_read_write(theme_connection, 0); + } + if (!unsupported) { - file_dialog_thread_abort.clear(); - file_dialog_thread.start(FreeDesktopPortalDesktop::_thread_file_dialog_monitor, this); + monitor_thread_abort.clear(); + monitor_thread.start(FreeDesktopPortalDesktop::_thread_monitor, this); } } FreeDesktopPortalDesktop::~FreeDesktopPortalDesktop() { - file_dialog_thread_abort.set(); - if (file_dialog_thread.is_started()) { - file_dialog_thread.wait_to_finish(); + monitor_thread_abort.set(); + if (monitor_thread.is_started()) { + monitor_thread.wait_to_finish(); } + for (FreeDesktopPortalDesktop::FileDialogData &fd : file_dialogs) { if (fd.connection) { DBusError err; @@ -631,6 +682,13 @@ FreeDesktopPortalDesktop::~FreeDesktopPortalDesktop() { dbus_connection_unref(fd.connection); } } + if (theme_connection) { + DBusError err; + dbus_error_init(&err); + dbus_bus_remove_match(theme_connection, theme_path.utf8().get_data(), &err); + dbus_error_free(&err); + dbus_connection_unref(theme_connection); + } } #endif // DBUS_ENABLED diff --git a/platform/linuxbsd/freedesktop_portal_desktop.h b/platform/linuxbsd/freedesktop_portal_desktop.h index c9da387241e..75afe02a261 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.h +++ b/platform/linuxbsd/freedesktop_portal_desktop.h @@ -34,6 +34,7 @@ #ifdef DBUS_ENABLED #include "core/os/thread.h" +#include "core/os/thread_safe.h" #include "servers/display_server.h" struct DBusMessage; @@ -68,10 +69,15 @@ private: Mutex file_dialog_mutex; Vector file_dialogs; - Thread file_dialog_thread; - SafeFlag file_dialog_thread_abort; + Thread monitor_thread; + SafeFlag monitor_thread_abort; - static void _thread_file_dialog_monitor(void *p_ud); + DBusConnection *theme_connection = nullptr; + String theme_path; + Callable system_theme_changed; + void _system_theme_changed_callback(); + + static void _thread_monitor(void *p_ud); public: FreeDesktopPortalDesktop(); @@ -86,6 +92,9 @@ public: // 1: Prefer dark appearance. // 2: Prefer light appearance. uint32_t get_appearance_color_scheme(); + void set_system_theme_change_callback(const Callable &p_system_theme_changed) { + system_theme_changed = p_system_theme_changed; + } }; #endif // DBUS_ENABLED diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index b8a10ea6b9e..85bbfe546a1 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -275,6 +275,10 @@ bool DisplayServerWayland::is_dark_mode() const { } } +void DisplayServerWayland::set_system_theme_change_callback(const Callable &p_callable) { + portal_desktop->set_system_theme_change_callback(p_callable); +} + Error DisplayServerWayland::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector &p_filters, const Callable &p_callback) { WindowID window_id = MAIN_WINDOW_ID; // TODO: Use window IDs for multiwindow support. diff --git a/platform/linuxbsd/wayland/display_server_wayland.h b/platform/linuxbsd/wayland/display_server_wayland.h index 58c5dab5867..d4da80a55f8 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.h +++ b/platform/linuxbsd/wayland/display_server_wayland.h @@ -171,6 +171,7 @@ public: #ifdef DBUS_ENABLED virtual bool is_dark_mode_supported() const override; virtual bool is_dark_mode() const override; + virtual void set_system_theme_change_callback(const Callable &p_callable) override; virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector &p_filters, const Callable &p_callback) override; virtual Error file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector &p_filters, const TypedArray &p_options, const Callable &p_callback) override; diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index b838e4b8705..35bfe81827a 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -364,6 +364,10 @@ bool DisplayServerX11::is_dark_mode() const { } } +void DisplayServerX11::set_system_theme_change_callback(const Callable &p_callable) { + portal_desktop->set_system_theme_change_callback(p_callable); +} + Error DisplayServerX11::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector &p_filters, const Callable &p_callback) { WindowID window_id = last_focused_window; diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h index 7c094d6a41a..a5cbe34d267 100644 --- a/platform/linuxbsd/x11/display_server_x11.h +++ b/platform/linuxbsd/x11/display_server_x11.h @@ -400,6 +400,7 @@ public: #if defined(DBUS_ENABLED) virtual bool is_dark_mode_supported() const override; virtual bool is_dark_mode() const override; + virtual void set_system_theme_change_callback(const Callable &p_callable) override; virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector &p_filters, const Callable &p_callback) override; virtual Error file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector &p_filters, const TypedArray &p_options, const Callable &p_callback) override; diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index 10c8abe663b..1b4426dc192 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -217,6 +217,8 @@ private: }; List deferred_menu_calls; + Callable system_theme_changed; + const NSMenu *_get_menu_root(const String &p_menu_root) const; NSMenu *_get_menu_root(const String &p_menu_root); bool _is_menu_opened(NSMenu *p_menu) const; @@ -251,6 +253,8 @@ public: void menu_open(NSMenu *p_menu); void menu_close(NSMenu *p_menu); + void emit_system_theme_changed(); + bool has_window(WindowID p_window) const; WindowData &get_window(WindowID p_window); @@ -351,6 +355,8 @@ public: virtual bool is_dark_mode_supported() const override; virtual bool is_dark_mode() const override; virtual Color get_accent_color() const override; + virtual Color get_base_color() const override; + virtual void set_system_theme_change_callback(const Callable &p_callable) override; virtual Error dialog_show(String p_title, String p_description, Vector p_buttons, const Callable &p_callback) override; virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index 19632dd7991..e5b65cef338 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -2037,7 +2037,17 @@ bool DisplayServerMacOS::is_dark_mode() const { Color DisplayServerMacOS::get_accent_color() const { if (@available(macOS 10.14, *)) { - NSColor *color = [[NSColor controlAccentColor] colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]]; + __block NSColor *color = nullptr; + if (@available(macOS 11.0, *)) { + [NSApp.effectiveAppearance performAsCurrentDrawingAppearance:^{ + color = [[NSColor controlAccentColor] colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]]; + }]; + } else { + NSAppearance *saved_appearance = [NSAppearance currentAppearance]; + [NSAppearance setCurrentAppearance:[NSApp effectiveAppearance]]; + color = [[NSColor controlAccentColor] colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]]; + [NSAppearance setCurrentAppearance:saved_appearance]; + } if (color) { CGFloat components[4]; [color getRed:&components[0] green:&components[1] blue:&components[2] alpha:&components[3]]; @@ -2050,6 +2060,41 @@ Color DisplayServerMacOS::get_accent_color() const { } } +Color DisplayServerMacOS::get_base_color() const { + if (@available(macOS 10.14, *)) { + __block NSColor *color = nullptr; + if (@available(macOS 11.0, *)) { + [NSApp.effectiveAppearance performAsCurrentDrawingAppearance:^{ + color = [[NSColor controlColor] colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]]; + }]; + } else { + NSAppearance *saved_appearance = [NSAppearance currentAppearance]; + [NSAppearance setCurrentAppearance:[NSApp effectiveAppearance]]; + color = [[NSColor controlColor] colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]]; + [NSAppearance setCurrentAppearance:saved_appearance]; + } + if (color) { + CGFloat components[4]; + [color getRed:&components[0] green:&components[1] blue:&components[2] alpha:&components[3]]; + return Color(components[0], components[1], components[2], components[3]); + } else { + return Color(0, 0, 0, 0); + } + } else { + return Color(0, 0, 0, 0); + } +} + +void DisplayServerMacOS::set_system_theme_change_callback(const Callable &p_callable) { + system_theme_changed = p_callable; +} + +void DisplayServerMacOS::emit_system_theme_changed() { + if (system_theme_changed.is_valid()) { + system_theme_changed.call(); + } +} + Error DisplayServerMacOS::dialog_show(String p_title, String p_description, Vector p_buttons, const Callable &p_callback) { _THREAD_SAFE_METHOD_ diff --git a/platform/macos/godot_application_delegate.mm b/platform/macos/godot_application_delegate.mm index c1de8ade58e..fb708c477fd 100644 --- a/platform/macos/godot_application_delegate.mm +++ b/platform/macos/godot_application_delegate.mm @@ -63,6 +63,13 @@ [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps]; } +- (void)system_theme_changed:(NSNotification *)notification { + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + if (ds) { + ds->emit_system_theme_changed(); + } +} + - (void)applicationDidFinishLaunching:(NSNotification *)notice { NSString *nsappname = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]; NSString *nsbundleid_env = [NSString stringWithUTF8String:getenv("__CFBundleIdentifier")]; @@ -71,6 +78,8 @@ // If the executable is started from terminal or is not bundled, macOS WindowServer won't register and activate app window correctly (menu and title bar are grayed out and input ignored). [self performSelector:@selector(forceUnbundledWindowActivationHackStep1) withObject:nil afterDelay:0.02]; } + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(system_theme_changed:) name:@"AppleInterfaceThemeChangedNotification" object:nil]; + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(system_theme_changed:) name:@"AppleColorPreferencesChangedNotification" object:nil]; } - (id)init { @@ -83,6 +92,11 @@ return self; } +- (void)dealloc { + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:@"AppleInterfaceThemeChangedNotification" object:nil]; + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:@"AppleColorPreferencesChangedNotification" object:nil]; +} + - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { OS_MacOS *os = (OS_MacOS *)OS::get_singleton(); if (!event || !os) { diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 96e2f95abd1..7d96bded14b 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -3494,13 +3494,17 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case WM_PAINT: { Main::force_redraw(); } break; - case WM_SETTINGCHANGE: { + case WM_SETTINGCHANGE: + case WM_SYSCOLORCHANGE: { if (lParam && CompareStringOrdinal(reinterpret_cast(lParam), -1, L"ImmersiveColorSet", -1, true) == CSTR_EQUAL) { if (is_dark_mode_supported() && dark_title_available) { BOOL value = is_dark_mode(); ::DwmSetWindowAttribute(windows[window_id].hWnd, use_legacy_dark_mode_before_20H1 ? DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 : DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value)); } } + if (system_theme_changed.is_valid()) { + system_theme_changed.call(); + } } break; case WM_THEMECHANGED: { if (is_dark_mode_supported() && dark_title_available) { @@ -5058,6 +5062,19 @@ Color DisplayServerWindows::get_accent_color() const { return Color((argb & 0xFF) / 255.f, ((argb & 0xFF00) >> 8) / 255.f, ((argb & 0xFF0000) >> 16) / 255.f, ((argb & 0xFF000000) >> 24) / 255.f); } +Color DisplayServerWindows::get_base_color() const { + if (!ux_theme_available) { + return Color(0, 0, 0, 0); + } + + int argb = GetImmersiveColorFromColorSetEx((UINT)GetImmersiveUserColorSetPreference(false, false), GetImmersiveColorTypeFromName(ShouldAppsUseDarkMode() ? L"ImmersiveDarkChromeMediumLow" : L"ImmersiveLightChromeMediumLow"), false, 0); + return Color((argb & 0xFF) / 255.f, ((argb & 0xFF00) >> 8) / 255.f, ((argb & 0xFF0000) >> 16) / 255.f, ((argb & 0xFF000000) >> 24) / 255.f); +} + +void DisplayServerWindows::set_system_theme_change_callback(const Callable &p_callable) { + system_theme_changed = p_callable; +} + int DisplayServerWindows::tablet_get_driver_count() const { return tablet_drivers.size(); } diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index e66c533da50..81cddec49f5 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -477,6 +477,8 @@ class DisplayServerWindows : public DisplayServer { CursorShape cursor_shape = CursorShape::CURSOR_ARROW; RBMap> cursors_cache; + Callable system_theme_changed; + void _drag_event(WindowID p_window, float p_x, float p_y, int idx); void _touch_event(WindowID p_window, bool p_pressed, float p_x, float p_y, int idx); @@ -522,6 +524,8 @@ public: virtual bool is_dark_mode_supported() const override; virtual bool is_dark_mode() const override; virtual Color get_accent_color() const override; + virtual Color get_base_color() const override; + virtual void set_system_theme_change_callback(const Callable &p_callable) override; virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector &p_filters, const Callable &p_callback) override; virtual Error file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector &p_filters, const TypedArray &p_options, const Callable &p_callback) override; diff --git a/servers/display_server.cpp b/servers/display_server.cpp index 0496fb9180b..0d9821a89f3 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -705,6 +705,8 @@ void DisplayServer::_bind_methods() { ClassDB::bind_method(D_METHOD("is_dark_mode_supported"), &DisplayServer::is_dark_mode_supported); ClassDB::bind_method(D_METHOD("is_dark_mode"), &DisplayServer::is_dark_mode); ClassDB::bind_method(D_METHOD("get_accent_color"), &DisplayServer::get_accent_color); + ClassDB::bind_method(D_METHOD("get_base_color"), &DisplayServer::get_base_color); + ClassDB::bind_method(D_METHOD("set_system_theme_change_callback", "callable"), &DisplayServer::set_system_theme_change_callback); ClassDB::bind_method(D_METHOD("mouse_set_mode", "mouse_mode"), &DisplayServer::mouse_set_mode); ClassDB::bind_method(D_METHOD("mouse_get_mode"), &DisplayServer::mouse_get_mode); diff --git a/servers/display_server.h b/servers/display_server.h index 6e5a4655a28..f52ba02bd96 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -224,7 +224,9 @@ public: virtual bool is_dark_mode_supported() const { return false; }; virtual bool is_dark_mode() const { return false; }; - virtual Color get_accent_color() const { return Color(0, 0, 0, 0); }; + virtual Color get_accent_color() const { return Color(0, 0, 0, 0); } + virtual Color get_base_color() const { return Color(0, 0, 0, 0); } + virtual void set_system_theme_change_callback(const Callable &p_callable) {} private: static bool window_early_clear_override_enabled;