From 47a237c9243e9597d44cb581802435a9aacdcb1e Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Tue, 14 Jul 2020 20:36:55 +0400 Subject: [PATCH] Implement system-based dark mode for Windows and Linux --- Telegram/SourceFiles/core/application.cpp | 19 ++++++++ Telegram/SourceFiles/core/core_settings.cpp | 9 +++- Telegram/SourceFiles/core/core_settings.h | 26 +++++++++++ .../SourceFiles/platform/linux/linux_libs.cpp | 43 +++++++++++-------- .../SourceFiles/platform/linux/linux_libs.h | 26 +++++++++++ .../platform/linux/specific_linux.cpp | 21 +++++++++ .../SourceFiles/platform/mac/specific_mac.h | 4 ++ .../SourceFiles/platform/platform_specific.h | 5 +++ .../SourceFiles/platform/win/specific_win.cpp | 25 +++++++++++ .../platform/win/windows_event_filter.cpp | 6 +++ .../SourceFiles/settings/settings_codes.cpp | 8 ++++ .../SourceFiles/window/window_main_menu.cpp | 13 ++++++ 12 files changed, 185 insertions(+), 20 deletions(-) diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index 19bd5000c..45ebbcb9c 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/local_url_handlers.h" #include "core/launcher.h" #include "core/ui_integration.h" +#include "core/core_settings.h" #include "chat_helpers/emoji_keywords.h" #include "chat_helpers/stickers_emoji_image_loader.h" #include "base/platform/base_platform_info.h" @@ -213,6 +214,23 @@ void Application::run() { startEmojiImageLoader(); Media::Player::start(_audio.get()); + const auto darkModeChanged = [] { + const auto darkMode = Core::App().settings().systemDarkMode(); + const auto darkModeEnabled = Core::App().settings().systemDarkModeEnabled(); + if (darkModeEnabled + && darkMode.has_value() + && (*darkMode != Window::Theme::IsNightMode())) { + Window::Theme::ToggleNightMode(); + Window::Theme::KeepApplied(); + } + }; + + Core::App().settings().systemDarkModeChanges( + ) | rpl::start_with_next(darkModeChanged, _lifetime); + + Core::App().settings().systemDarkModeEnabledChanges( + ) | rpl::start_with_next(darkModeChanged, _lifetime); + style::ShortAnimationPlaying( ) | rpl::start_with_next([=](bool playing) { if (playing) { @@ -235,6 +253,7 @@ void Application::run() { _domain->activeChanges( ) | rpl::start_with_next([=](not_null account) { _window->showAccount(account); + Core::App().settings().setSystemDarkMode(Platform::IsDarkMode()); }, _window->widget()->lifetime()); QCoreApplication::instance()->installEventFilter(this); diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 1988fec58..5bceaa88b 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -106,7 +106,8 @@ QByteArray Settings::serialize() const { << qint32(_thirdColumnWidth.current()) << qint32(_thirdSectionExtendedBy) << qint32(_notifyFromAll ? 1 : 0) - << qint32(_nativeWindowFrame.current() ? 1 : 0); + << qint32(_nativeWindowFrame.current() ? 1 : 0) + << qint32(_systemDarkModeEnabled.current() ? 1 : 0); } return result; } @@ -171,6 +172,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { qint32 thirdSectionExtendedBy = _thirdSectionExtendedBy; qint32 notifyFromAll = _notifyFromAll ? 1 : 0; qint32 nativeWindowFrame = _nativeWindowFrame.current() ? 1 : 0; + qint32 systemDarkModeEnabled = _systemDarkModeEnabled.current() ? 1 : 0; stream >> themesAccentColors; if (!stream.atEnd()) { @@ -248,6 +250,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) { if (!stream.atEnd()) { stream >> nativeWindowFrame; } + if (!stream.atEnd()) { + stream >> systemDarkModeEnabled; + } if (stream.status() != QDataStream::Ok) { LOG(("App Error: " "Bad data for Core::Settings::constructFromSerialized()")); @@ -341,6 +346,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) { } _notifyFromAll = (notifyFromAll == 1); _nativeWindowFrame = (nativeWindowFrame == 1); + _systemDarkModeEnabled = (systemDarkModeEnabled == 1); } bool Settings::chatWide() const { @@ -474,6 +480,7 @@ void Settings::resetOnLastLogout() { _thirdColumnWidth = kDefaultThirdColumnWidth; // p-w _notifyFromAll = true; _tabbedReplacedWithInfo = false; // per-window + _systemDarkModeEnabled = false; } bool Settings::ThirdColumnByDefault() { diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 144b4977c..b7560cf38 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -416,6 +416,30 @@ public: [[nodiscard]] rpl::producer nativeWindowFrameChanges() const { return _nativeWindowFrame.changes(); } + void setSystemDarkMode(std::optional value) { + _systemDarkMode = value; + } + [[nodiscard]] std::optional systemDarkMode() const { + return _systemDarkMode.current(); + } + [[nodiscard]] rpl::producer> systemDarkModeValue() const { + return _systemDarkMode.value(); + } + [[nodiscard]] rpl::producer> systemDarkModeChanges() const { + return _systemDarkMode.changes(); + } + void setSystemDarkModeEnabled(bool value) { + _systemDarkModeEnabled = value; + } + [[nodiscard]] bool systemDarkModeEnabled() const { + return _systemDarkModeEnabled.current(); + } + [[nodiscard]] rpl::producer systemDarkModeEnabledValue() const { + return _systemDarkModeEnabled.value(); + } + [[nodiscard]] rpl::producer systemDarkModeEnabledChanges() const { + return _systemDarkModeEnabled.changes(); + } [[nodiscard]] static bool ThirdColumnByDefault(); [[nodiscard]] float64 DefaultDialogsWidthRatio(); @@ -484,6 +508,8 @@ private: rpl::variable _thirdColumnWidth = kDefaultThirdColumnWidth; // p-w bool _notifyFromAll = true; rpl::variable _nativeWindowFrame = false; + rpl::variable> _systemDarkMode = std::nullopt; + rpl::variable _systemDarkModeEnabled = false; bool _tabbedReplacedWithInfo = false; // per-window rpl::event_stream _tabbedReplacedWithInfoValue; // per-window diff --git a/Telegram/SourceFiles/platform/linux/linux_libs.cpp b/Telegram/SourceFiles/platform/linux/linux_libs.cpp index 0c8cb32c0..c6590d3e0 100644 --- a/Telegram/SourceFiles/platform/linux/linux_libs.cpp +++ b/Telegram/SourceFiles/platform/linux/linux_libs.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/linux/linux_desktop_environment.h" #include "platform/linux/specific_linux.h" #include "core/sandbox.h" +#include "core/core_settings.h" #include "core/application.h" #include "main/main_domain.h" #include "mainwindow.h" @@ -22,6 +23,7 @@ namespace Libs { namespace { bool gtkTriedToInit = false; +bool gtkLoaded = false; bool loadLibrary(QLibrary &lib, const char *name, int version) { #if defined DESKTOP_APP_USE_PACKAGED && !defined DESKTOP_APP_USE_PACKAGED_LAZY @@ -44,21 +46,6 @@ bool loadLibrary(QLibrary &lib, const char *name, int version) { } #ifndef TDESKTOP_DISABLE_GTK_INTEGRATION -template -T gtkSetting(const gchar *propertyName) { - GtkSettings *settings = gtk_settings_get_default(); - T value; - g_object_get(settings, propertyName, &value, nullptr); - return value; -} - -QString gtkSetting(const gchar *propertyName) { - gchararray value = gtkSetting(propertyName); - QString str = QString::fromUtf8(value); - g_free(value); - return str; -} - void gtkMessageHandler( const gchar *log_domain, GLogLevelFlags log_level, @@ -75,6 +62,7 @@ void gtkMessageHandler( bool setupGtkBase(QLibrary &lib_gtk) { if (!LOAD_SYMBOL(lib_gtk, "gtk_init_check", gtk_init_check)) return false; + if (!LOAD_SYMBOL(lib_gtk, "gtk_check_version", gtk_check_version)) return false; if (!LOAD_SYMBOL(lib_gtk, "gtk_settings_get_default", gtk_settings_get_default)) return false; if (!LOAD_SYMBOL(lib_gtk, "gtk_widget_show", gtk_widget_show)) return false; @@ -173,10 +161,12 @@ bool IconThemeShouldBeSet() { void SetIconTheme() { Core::Sandbox::Instance().customEnterFromEventLoop([] { - if (IconThemeShouldBeSet()) { + if (GtkSettingSupported() + && GtkLoaded() + && IconThemeShouldBeSet()) { DEBUG_LOG(("Set GTK icon theme")); - QIcon::setThemeName(gtkSetting("gtk-icon-theme-name")); - QIcon::setFallbackThemeName(gtkSetting("gtk-fallback-icon-theme")); + QIcon::setThemeName(GtkSetting("gtk-icon-theme-name")); + QIcon::setFallbackThemeName(GtkSetting("gtk-fallback-icon-theme")); Platform::SetApplicationIcon(Window::CreateIcon()); if (App::wnd()) { App::wnd()->setWindowIcon(Window::CreateIcon()); @@ -185,12 +175,19 @@ void SetIconTheme() { } }); } + +void DarkModeChanged() { + Core::Sandbox::Instance().customEnterFromEventLoop([] { + Core::App().settings().setSystemDarkMode(Platform::IsDarkMode()); + }); +} #endif // !TDESKTOP_DISABLE_GTK_INTEGRATION } // namespace #ifndef TDESKTOP_DISABLE_GTK_INTEGRATION f_gtk_init_check gtk_init_check = nullptr; +f_gtk_check_version gtk_check_version = nullptr; f_gtk_settings_get_default gtk_settings_get_default = nullptr; f_gtk_widget_show gtk_widget_show = nullptr; f_gtk_widget_hide gtk_widget_hide = nullptr; @@ -245,6 +242,10 @@ f_gdk_pixbuf_get_pixels gdk_pixbuf_get_pixels = nullptr; f_gdk_pixbuf_get_width gdk_pixbuf_get_width = nullptr; f_gdk_pixbuf_get_height gdk_pixbuf_get_height = nullptr; f_gdk_pixbuf_get_rowstride gdk_pixbuf_get_rowstride = nullptr; + +bool GtkLoaded() { + return gtkLoaded; +} #endif // !TDESKTOP_DISABLE_GTK_INTEGRATION void start() { @@ -255,7 +256,6 @@ void start() { DEBUG_LOG(("Loading libraries")); - bool gtkLoaded = false; QLibrary lib_gtk; lib_gtk.setLoadHints(QLibrary::DeepBindHint); @@ -284,6 +284,11 @@ void start() { const auto settings = gtk_settings_get_default(); g_signal_connect(settings, "notify::gtk-icon-theme-name", G_CALLBACK(SetIconTheme), nullptr); + g_signal_connect(settings, "notify::gtk-theme-name", G_CALLBACK(DarkModeChanged), nullptr); + + if (!gtk_check_version(3, 0, 0)) { + g_signal_connect(settings, "notify::gtk-application-prefer-dark-theme", G_CALLBACK(DarkModeChanged), nullptr); + } } else { LOG(("Could not load gtk-3 or gtk-x11-2.0!")); } diff --git a/Telegram/SourceFiles/platform/linux/linux_libs.h b/Telegram/SourceFiles/platform/linux/linux_libs.h index bd2c576fe..9ad3884ea 100644 --- a/Telegram/SourceFiles/platform/linux/linux_libs.h +++ b/Telegram/SourceFiles/platform/linux/linux_libs.h @@ -29,6 +29,10 @@ extern "C" { namespace Platform { namespace Libs { +#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION +bool GtkLoaded(); +#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION + void start(); template @@ -50,6 +54,9 @@ bool load(QLibrary &lib, const char *name, Function &func) { typedef gboolean (*f_gtk_init_check)(int *argc, char ***argv); extern f_gtk_init_check gtk_init_check; +typedef const gchar* (*f_gtk_check_version)(guint required_major, guint required_minor, guint required_micro); +extern f_gtk_check_version gtk_check_version; + typedef GtkSettings* (*f_gtk_settings_get_default)(void); extern f_gtk_settings_get_default gtk_settings_get_default; @@ -252,6 +259,25 @@ extern f_gdk_pixbuf_get_height gdk_pixbuf_get_height; typedef int (*f_gdk_pixbuf_get_rowstride)(const GdkPixbuf *pixbuf); extern f_gdk_pixbuf_get_rowstride gdk_pixbuf_get_rowstride; + +inline bool GtkSettingSupported() { + return gtk_settings_get_default != nullptr; +} + +template +inline T GtkSetting(const gchar *propertyName) { + GtkSettings *settings = gtk_settings_get_default(); + T value; + g_object_get(settings, propertyName, &value, nullptr); + return value; +} + +inline QString GtkSetting(const gchar *propertyName) { + gchararray value = GtkSetting(propertyName); + QString str = QString::fromUtf8(value); + g_free(value); + return str; +} #endif // !TDESKTOP_DISABLE_GTK_INTEGRATION } // namespace Libs diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index d366cf1c2..79825ac92 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -869,6 +869,27 @@ std::optional LastUserInputTime() { return std::nullopt; } +std::optional IsDarkMode() { +#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION + if (Libs::GtkSettingSupported() + && Libs::GtkLoaded()) { + if (Libs::gtk_check_version != nullptr + && !Libs::gtk_check_version(3, 0, 0) + && Libs::GtkSetting("gtk-application-prefer-dark-theme")) { + return true; + } + + if (Libs::GtkSetting("gtk-theme-name").toLower().endsWith(qsl("-dark"))) { + return true; + } + + return false; + } +#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION + + return std::nullopt; +} + bool AutostartSupported() { // snap sandbox doesn't allow creating files in folders with names started with a dot // and doesn't provide any api to add an app to autostart diff --git a/Telegram/SourceFiles/platform/mac/specific_mac.h b/Telegram/SourceFiles/platform/mac/specific_mac.h index 0d3b90c0b..f749b44a3 100644 --- a/Telegram/SourceFiles/platform/mac/specific_mac.h +++ b/Telegram/SourceFiles/platform/mac/specific_mac.h @@ -18,6 +18,10 @@ namespace Platform { void RemoveQuarantine(const QString &path); +inline std::optional IsDarkMode() { + return std::nullopt; +} + inline void FallbackFontConfigCheckBegin() { } diff --git a/Telegram/SourceFiles/platform/platform_specific.h b/Telegram/SourceFiles/platform/platform_specific.h index 5172e88e8..28c73dcb8 100644 --- a/Telegram/SourceFiles/platform/platform_specific.h +++ b/Telegram/SourceFiles/platform/platform_specific.h @@ -41,6 +41,11 @@ bool OpenSystemSettings(SystemSettingsType type); return LastUserInputTime().has_value(); } +[[nodiscard]] std::optional IsDarkMode(); +[[nodiscard]] inline bool IsDarkModeSupported() { + return IsDarkMode().has_value(); +} + void IgnoreApplicationActivationRightNow(); bool AutostartSupported(); QImage GetImageFromClipboard(); diff --git a/Telegram/SourceFiles/platform/win/specific_win.cpp b/Telegram/SourceFiles/platform/win/specific_win.cpp index a9fb43816..acfad2153 100644 --- a/Telegram/SourceFiles/platform/win/specific_win.cpp +++ b/Telegram/SourceFiles/platform/win/specific_win.cpp @@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localstorage.h" #include "core/crash_reports.h" +#include #include #include #include @@ -382,6 +383,30 @@ std::optional LastUserInputTime() { return LastTrackedWhen; } +std::optional IsDarkMode() { + if (QOperatingSystemVersion::current() + < QOperatingSystemVersion(QOperatingSystemVersion::Windows, 10, 0, 17763)) { + return std::nullopt; + } + + LPCWSTR lpKeyName = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; + LPCWSTR lpValueName = L"AppsUseLightTheme"; + HKEY key; + auto result = RegOpenKeyEx(HKEY_CURRENT_USER, lpKeyName, 0, KEY_READ, &key); + if (result != ERROR_SUCCESS) { + return std::nullopt; + } + + DWORD value = 0, type = 0, size = sizeof(value); + result = RegQueryValueEx(key, lpValueName, 0, &type, (LPBYTE)&value, &size); + RegCloseKey(key); + if (result != ERROR_SUCCESS) { + return std::nullopt; + } + + return value == 0; +} + bool AutostartSupported() { return !IsWindowsStoreBuild(); } diff --git a/Telegram/SourceFiles/platform/win/windows_event_filter.cpp b/Telegram/SourceFiles/platform/win/windows_event_filter.cpp index 6f0d8f901..52eb9a8e5 100644 --- a/Telegram/SourceFiles/platform/win/windows_event_filter.cpp +++ b/Telegram/SourceFiles/platform/win/windows_event_filter.cpp @@ -8,7 +8,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/win/windows_event_filter.h" #include "platform/win/windows_dlls.h" +#include "platform/win/specific_win.h" #include "core/sandbox.h" +#include "core/core_settings.h" #include "core/application.h" #include "ui/inactive_press.h" #include "mainwindow.h" @@ -274,6 +276,10 @@ bool EventFilter::mainWindowEvent( } } return true; + case WM_SETTINGCHANGE: { + Core::App().settings().setSystemDarkMode(Platform::IsDarkMode()); + } return false; + } return false; } diff --git a/Telegram/SourceFiles/settings/settings_codes.cpp b/Telegram/SourceFiles/settings/settings_codes.cpp index dd0243472..9b4ce3a21 100644 --- a/Telegram/SourceFiles/settings/settings_codes.cpp +++ b/Telegram/SourceFiles/settings/settings_codes.cpp @@ -120,6 +120,14 @@ auto GenerateCodes() { window->showSettings(Settings::Type::Folders); } }); + codes.emplace(qsl("autodark"), [](SessionController *window) { + auto text = Core::App().settings().systemDarkModeEnabled() ? qsl("Disable system dark mode?") : qsl("Enable system dark mode?"); + Ui::show(Box(text, [=] { + Core::App().settings().setSystemDarkModeEnabled(!Core::App().settings().systemDarkModeEnabled()); + Core::App().saveSettingsDelayed(); + Ui::hideLayer(); + })); + }); #ifndef TDESKTOP_DISABLE_REGISTER_CUSTOM_SCHEME codes.emplace(qsl("registertg"), [](SessionController *window) { diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 1ca3265f4..c8721e69c 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -902,6 +902,19 @@ void MainMenu::refreshMenu() { *_nightThemeAction = action; action->setCheckable(true); action->setChecked(Window::Theme::IsNightMode()); + Core::App().settings().systemDarkModeValue( + ) | rpl::start_with_next([=](std::optional darkMode) { + const auto darkModeEnabled = Core::App().settings().systemDarkModeEnabled(); + if (darkModeEnabled && darkMode.has_value()) { + action->setChecked(*darkMode); + } + action->setEnabled(!darkModeEnabled || !darkMode.has_value()); + }, lifetime()); + Core::App().settings().systemDarkModeEnabledChanges( + ) | rpl::start_with_next([=](bool darkModeEnabled) { + const auto darkMode = Core::App().settings().systemDarkMode(); + action->setEnabled(!darkModeEnabled || !darkMode.has_value()); + }, lifetime()); _menu->finishAnimating(); updatePhone();