diff --git a/changes.yaml b/changes.yaml index a058f00f7..a9f2cace0 100644 --- a/changes.yaml +++ b/changes.yaml @@ -1,5 +1,13 @@ --- releases: + - version: 1.11.0-beta.15 + date: 2024-09-22T00:00:00+02:00 + short: |- + Workrave 1.11.0-beta.15 has been released. + changes: + - Follow OS dark/light mode on Windows (#529) + - Fix dark mode on Windows (#537) + - version: 1.11.0-beta.14 date: 2024-06-15T14:29:23+02:00 short: |- @@ -13,7 +21,7 @@ releases: short: |- Workrave 1.11.0-beta.13 has been released. changes: - - Improved handling of hibernate/suspend (#539, #540) + - Improved handling of hibernate/suspend (#539,#540) - version: 1.11.0-beta.12 date: 2024-03-21T19:48:51+01:00 @@ -165,11 +173,11 @@ releases: - Major internal code cleanup and code modernization. - Initial (incomplete) port to macOS using Qt6 - The default sound theme was changed to the "Subtle" theme (#362) - - You can now change the operation mode for a limited amount of time (#98, #305) + - You can now change the operation mode for a limited amount of time (#98,#305) - A portable version is now available for Windows and Linux (#180) - Workrave now follows the XDG basedirectory specification (#192) - The Windows version is now 64 bit (#295) - - Workrave can now automatically change the operation mode based on Windows Focus Assist (#255, #226) + - Workrave can now automatically change the operation mode based on Windows Focus Assist (#255,#226) - A dark UI theme is now supported on Windows (#214) - |- Bug fixes: diff --git a/ui/app/GUIConfig.cc b/ui/app/GUIConfig.cc index 1a509d584..7daecfc03 100644 --- a/ui/app/GUIConfig.cc +++ b/ui/app/GUIConfig.cc @@ -26,44 +26,44 @@ #include "core/ICore.hh" #include "core/IBreak.hh" -using namespace std; using namespace workrave::config; -const string GUIConfig::CFG_KEY_BREAK_IGNORABLE = "gui/breaks/%b/ignorable_break"; -const string GUIConfig::CFG_KEY_BREAK_SKIPPABLE = "gui/breaks/%b/skippable_break"; -const string GUIConfig::CFG_KEY_BREAK_ENABLE_SHUTDOWN = "gui/breaks/%b/enable_shutdown"; -const string GUIConfig::CFG_KEY_BREAK_EXERCISES = "gui/breaks/%b/exercises"; -const string GUIConfig::CFG_KEY_BREAK_AUTO_NATURAL = "gui/breaks/%b/auto_natural"; -const string GUIConfig::CFG_KEY_BLOCK_MODE = "gui/breaks/block_mode"; -const string GUIConfig::CFG_KEY_FOCUS_MODE = "gui/breaks/focus_mode"; -const string GUIConfig::CFG_KEY_FOLLOW_FOCUS_ASSIST_ENABLED = "gui/breaks/follow_focus_assist_enabled"; -const string GUIConfig::CFG_KEY_LOCALE = "gui/locale"; -const string GUIConfig::CFG_KEY_TRAYICON_ENABLED = "gui/trayicon_enabled"; -const string GUIConfig::CFG_KEY_CLOSEWARN_ENABLED = "gui/closewarn_enabled"; -const string GUIConfig::CFG_KEY_AUTOSTART = "gui/autostart"; -const string GUIConfig::CFG_KEY_ICONTHEME = "gui/icontheme"; -const string GUIConfig::CFG_KEY_THEME_NAME = "gui/theme_name"; -const string GUIConfig::CFG_KEY_THEME_DARK = "gui/theme_dark"; -const string GUIConfig::CFG_KEY_FORCE_X11 = "gui/force_x11"; - -const string GUIConfig::CFG_KEY_MAIN_WINDOW = "gui/main_window"; -const string GUIConfig::CFG_KEY_MAIN_WINDOW_ALWAYS_ON_TOP = "gui/main_window/always_on_top"; -const string GUIConfig::CFG_KEY_MAIN_WINDOW_START_IN_TRAY = "gui/main_window/start_in_tray"; -const string GUIConfig::CFG_KEY_MAIN_WINDOW_X = "gui/main_window/x"; -const string GUIConfig::CFG_KEY_MAIN_WINDOW_Y = "gui/main_window/y"; -const string GUIConfig::CFG_KEY_MAIN_WINDOW_HEAD = "gui/main_window/head"; - -const string GUIConfig::CFG_KEY_APPLET_FALLBACK_ENABLED = "gui/applet/fallback_enabled"; -const string GUIConfig::CFG_KEY_APPLET_ICON_ENABLED = "gui/applet/icon_enabled"; - -const string GUIConfig::CFG_KEY_TIMERBOX = "gui/"; -const string GUIConfig::CFG_KEY_TIMERBOX_CYCLE_TIME = "/cycle_time"; -const string GUIConfig::CFG_KEY_TIMERBOX_ENABLED = "/enabled"; -const string GUIConfig::CFG_KEY_TIMERBOX_POSITION = "/position"; -const string GUIConfig::CFG_KEY_TIMERBOX_FLAGS = "/flags"; -const string GUIConfig::CFG_KEY_TIMERBOX_IMMINENT = "/imminent"; - -string +const std::string GUIConfig::CFG_KEY_BREAK_IGNORABLE = "gui/breaks/%b/ignorable_break"; +const std::string GUIConfig::CFG_KEY_BREAK_SKIPPABLE = "gui/breaks/%b/skippable_break"; +const std::string GUIConfig::CFG_KEY_BREAK_ENABLE_SHUTDOWN = "gui/breaks/%b/enable_shutdown"; +const std::string GUIConfig::CFG_KEY_BREAK_EXERCISES = "gui/breaks/%b/exercises"; +const std::string GUIConfig::CFG_KEY_BREAK_AUTO_NATURAL = "gui/breaks/%b/auto_natural"; +const std::string GUIConfig::CFG_KEY_BLOCK_MODE = "gui/breaks/block_mode"; +const std::string GUIConfig::CFG_KEY_FOCUS_MODE = "gui/breaks/focus_mode"; +const std::string GUIConfig::CFG_KEY_FOLLOW_FOCUS_ASSIST_ENABLED = "gui/breaks/follow_focus_assist_enabled"; +const std::string GUIConfig::CFG_KEY_LOCALE = "gui/locale"; +const std::string GUIConfig::CFG_KEY_TRAYICON_ENABLED = "gui/trayicon_enabled"; +const std::string GUIConfig::CFG_KEY_CLOSEWARN_ENABLED = "gui/closewarn_enabled"; +const std::string GUIConfig::CFG_KEY_AUTOSTART = "gui/autostart"; +const std::string GUIConfig::CFG_KEY_ICONTHEME = "gui/icontheme"; +const std::string GUIConfig::CFG_KEY_THEME_NAME = "gui/theme_name"; +const std::string GUIConfig::CFG_KEY_THEME_DARK = "gui/theme_dark"; +const std::string GUIConfig::CFG_KEY_LIGHT_DARK_MODE = "gui/light_dark_mode"; +const std::string GUIConfig::CFG_KEY_FORCE_X11 = "gui/force_x11"; + +const std::string GUIConfig::CFG_KEY_MAIN_WINDOW = "gui/main_window"; +const std::string GUIConfig::CFG_KEY_MAIN_WINDOW_ALWAYS_ON_TOP = "gui/main_window/always_on_top"; +const std::string GUIConfig::CFG_KEY_MAIN_WINDOW_START_IN_TRAY = "gui/main_window/start_in_tray"; +const std::string GUIConfig::CFG_KEY_MAIN_WINDOW_X = "gui/main_window/x"; +const std::string GUIConfig::CFG_KEY_MAIN_WINDOW_Y = "gui/main_window/y"; +const std::string GUIConfig::CFG_KEY_MAIN_WINDOW_HEAD = "gui/main_window/head"; + +const std::string GUIConfig::CFG_KEY_APPLET_FALLBACK_ENABLED = "gui/applet/fallback_enabled"; +const std::string GUIConfig::CFG_KEY_APPLET_ICON_ENABLED = "gui/applet/icon_enabled"; + +const std::string GUIConfig::CFG_KEY_TIMERBOX = "gui/"; +const std::string GUIConfig::CFG_KEY_TIMERBOX_CYCLE_TIME = "/cycle_time"; +const std::string GUIConfig::CFG_KEY_TIMERBOX_ENABLED = "/enabled"; +const std::string GUIConfig::CFG_KEY_TIMERBOX_POSITION = "/position"; +const std::string GUIConfig::CFG_KEY_TIMERBOX_FLAGS = "/flags"; +const std::string GUIConfig::CFG_KEY_TIMERBOX_IMMINENT = "/imminent"; + +std::string GUIConfig::get_break_name(workrave::BreakId id) { std::array names{"micro_pause", "rest_break", "daily_limit"}; @@ -101,16 +101,20 @@ GUIConfig::init(std::shared_ptr config) config->set_value(CFG_KEY_CLOSEWARN_ENABLED, true, workrave::config::CONFIG_FLAG_INITIAL); config->set_value(CFG_KEY_AUTOSTART, true, CONFIG_FLAG_INITIAL); config->set_value(CFG_KEY_LOCALE, "", CONFIG_FLAG_INITIAL); + + bool dark = false; + config->get_value_with_default(CFG_KEY_THEME_DARK, dark, false); + config->set_value(CFG_KEY_LIGHT_DARK_MODE, dark ? 1 : 0, CONFIG_FLAG_INITIAL); } std::string GUIConfig::expand(const std::string &key, workrave::BreakId id) { - string str = key; - string::size_type pos = 0; - string name = GUIConfig::get_break_name(id); + std::string str = key; + std::string::size_type pos = 0; + std::string name = GUIConfig::get_break_name(id); - while ((pos = str.find("%b", pos)) != string::npos) + while ((pos = str.find("%b", pos)) != std::string::npos) { str.replace(pos, 2, name); pos++; @@ -198,15 +202,15 @@ GUIConfig::icon_theme() -> Setting & } auto -GUIConfig::theme_dark() -> workrave::config::Setting & +GUIConfig::theme_name() -> workrave::config::Setting & { - return SettingCache::get(config, CFG_KEY_THEME_DARK, false); + return SettingCache::get(config, CFG_KEY_THEME_NAME, std::string()); } auto -GUIConfig::theme_name() -> workrave::config::Setting & +GUIConfig::light_dark_mode() -> workrave::config::Setting & { - return SettingCache::get(config, CFG_KEY_THEME_NAME, std::string()); + return SettingCache::get(config, CFG_KEY_LIGHT_DARK_MODE, LightDarkTheme::Auto); } auto diff --git a/ui/app/include/ui/GUIConfig.hh b/ui/app/include/ui/GUIConfig.hh index 53c5fe721..91feee04b 100644 --- a/ui/app/include/ui/GUIConfig.hh +++ b/ui/app/include/ui/GUIConfig.hh @@ -21,11 +21,9 @@ #include #include "config/IConfigurator.hh" -#include "core/ICore.hh" +#include "core/CoreTypes.hh" #include "config/Setting.hh" -#include "ui/IApplicationContext.hh" - enum class BlockMode { Off = 0, @@ -39,6 +37,13 @@ enum class FocusMode Quiet }; +enum class LightDarkTheme +{ + Light, + Dark, + Auto +}; + class GUIConfig { public: @@ -65,8 +70,8 @@ public: static workrave::config::Setting &closewarn_enabled(); static workrave::config::Setting &autostart_enabled(); static workrave::config::Setting &icon_theme(); - static workrave::config::Setting &theme_dark(); static workrave::config::Setting &theme_name(); + static workrave::config::Setting &light_dark_mode(); static workrave::config::Setting &force_x11(); static workrave::config::Setting &main_window_always_on_top(); @@ -105,6 +110,7 @@ private: static const std::string CFG_KEY_ICONTHEME; static const std::string CFG_KEY_THEME_NAME; static const std::string CFG_KEY_THEME_DARK; + static const std::string CFG_KEY_LIGHT_DARK_MODE; static const std::string CFG_KEY_FORCE_X11; static const std::string CFG_KEY_MAIN_WINDOW; diff --git a/ui/app/toolkits/gtkmm/AutoUpdateDialog.cc b/ui/app/toolkits/gtkmm/AutoUpdateDialog.cc index 68c4c48db..ab487463d 100644 --- a/ui/app/toolkits/gtkmm/AutoUpdateDialog.cc +++ b/ui/app/toolkits/gtkmm/AutoUpdateDialog.cc @@ -40,6 +40,7 @@ #if defined(PLATFORM_OS_WINDOWS) # include "cmark.h" # include "Edge.hh" +# include "ToolkitWindows.hh" static constexpr const char *doc = R"( @@ -171,7 +172,21 @@ AutoUpdateDialog::AutoUpdateDialog(std::shared_ptr info, Aut } } - web->set_content(fmt::format(doc, GUIConfig::theme_dark()() ? darkstyle : lightstyle, body)); + bool dark = false; + switch (GUIConfig::light_dark_mode()()) + { + case LightDarkTheme::Light: + dark = false; + break; + case LightDarkTheme::Dark: + dark = true; + break; + case LightDarkTheme::Auto: + dark = ToolkitWindows::is_windows_app_theme_dark(); + break; + } + + web->set_content(fmt::format(doc, dark ? darkstyle : lightstyle, body)); notes_frame->add(*web); } else diff --git a/ui/app/toolkits/gtkmm/ToolkitWindows.cc b/ui/app/toolkits/gtkmm/ToolkitWindows.cc index dbe479bed..f88ac5684 100644 --- a/ui/app/toolkits/gtkmm/ToolkitWindows.cc +++ b/ui/app/toolkits/gtkmm/ToolkitWindows.cc @@ -21,6 +21,8 @@ #include "ToolkitWindows.hh" +#include +#include #include #ifndef PLATFORM_OS_WINDOWS_NATIVE @@ -58,7 +60,8 @@ ToolkitWindows::init(std::shared_ptr app) // No auto hide scrollbars g_setenv("GTK_OVERLAY_SCROLLING", "0", TRUE); // No Windows-7 style client-side decorations on Windows 10... - g_setenv("GTK_CSD", "0", TRUE); + // TODO: check if still needed. + // g_setenv("GTK_CSD", "0", TRUE); Toolkit::init(app); @@ -96,22 +99,39 @@ void ToolkitWindows::init_gui() { auto settings = Gtk::Settings::get_default(); - settings->property_gtk_application_prefer_dark_theme().set_value(GUIConfig::theme_dark()()); - std::string theme_name = GUIConfig::theme_name()(); - if (!theme_name.empty()) - { - settings->property_gtk_theme_name().set_value(theme_name); - } - settings->property_gtk_application_prefer_dark_theme().signal_changed().connect( - [settings]() { GUIConfig::theme_dark().set(settings->property_gtk_application_prefer_dark_theme().get_value()); }); - settings->property_gtk_theme_name().signal_changed().connect( - [settings]() { GUIConfig::theme_name().set(settings->property_gtk_theme_name().get_value()); }); + GUIConfig::light_dark_mode().attach(tracker, [settings](auto dark) { + switch (dark) + + { + case LightDarkTheme::Light: + settings->property_gtk_application_prefer_dark_theme().set_value(false); + break; + case LightDarkTheme::Dark: + settings->property_gtk_application_prefer_dark_theme().set_value(true); + break; + case LightDarkTheme::Auto: + settings->property_gtk_application_prefer_dark_theme().set_value(is_windows_app_theme_dark()); + break; + } + }); + + GUIConfig::theme_name().attach(tracker, [settings](auto name) { + if (!name.empty()) + { + settings->property_gtk_theme_name().set_value(name); + } + }); - GUIConfig::theme_dark().connect(tracker, [settings](auto dark) { - settings->property_gtk_application_prefer_dark_theme().set_value(dark); + settings->property_gtk_application_prefer_dark_theme().signal_changed().connect([settings]() { + if (GUIConfig::light_dark_mode()() != LightDarkTheme::Auto) + { + GUIConfig::light_dark_mode().set( + settings->property_gtk_application_prefer_dark_theme().get_value() ? LightDarkTheme::Dark : LightDarkTheme::Light); + } }); - GUIConfig::theme_name().connect(tracker, [settings](auto name) { settings->property_gtk_theme_name().set_value(name); }); + settings->property_gtk_theme_name().signal_changed().connect( + [settings]() { GUIConfig::theme_name().set(settings->property_gtk_theme_name().get_value()); }); } void @@ -229,6 +249,19 @@ ToolkitWindows::filter_func(MSG *msg) } break; + case WM_SETTINGCHANGE: + { + if (msg->lParam != 0 && _wcsicmp(L"ImmersiveColorSet", reinterpret_cast(msg->lParam)) == 0) + { + if (GUIConfig::light_dark_mode()() == LightDarkTheme::Auto) + { + logger->info("Theme change detected: switching to {} theme", is_windows_app_theme_dark() ? "dark" : "light"); + auto settings = Gtk::Settings::get_default(); + settings->property_gtk_application_prefer_dark_theme().set_value(is_windows_app_theme_dark()); + } + } + } + case WM_DEVICECHANGE: { TRACE_MSG("WM_DEVICECHANGE {} {}", msg->wParam, msg->lParam); @@ -268,3 +301,18 @@ ToolkitWindows::get_locker() { return locker; } + +bool +ToolkitWindows::is_windows_app_theme_dark() +{ + DWORD value = 1; // Default to light theme + DWORD dataSize = sizeof(value); + HKEY hKey = nullptr; + if (RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", 0, KEY_READ, &hKey) + == ERROR_SUCCESS) + { + RegQueryValueExW(hKey, L"AppsUseLightTheme", NULL, NULL, (LPBYTE)&value, &dataSize); + RegCloseKey(hKey); + } + return value == 0; +} diff --git a/ui/app/toolkits/gtkmm/ToolkitWindows.hh b/ui/app/toolkits/gtkmm/ToolkitWindows.hh index 593fcfb8a..f4958946e 100644 --- a/ui/app/toolkits/gtkmm/ToolkitWindows.hh +++ b/ui/app/toolkits/gtkmm/ToolkitWindows.hh @@ -48,6 +48,8 @@ public: boost::signals2::signal &hook_event() override; HWND get_event_hwnd() const override; + static bool is_windows_app_theme_dark(); + private: void init_filter(); void init_gui(); diff --git a/ui/app/toolkits/gtkmm/preferences/GeneralPreferencePanel.cc b/ui/app/toolkits/gtkmm/preferences/GeneralPreferencePanel.cc index 7a66fa532..266a15447 100644 --- a/ui/app/toolkits/gtkmm/preferences/GeneralPreferencePanel.cc +++ b/ui/app/toolkits/gtkmm/preferences/GeneralPreferencePanel.cc @@ -234,22 +234,64 @@ GeneralPreferencePanel::create_panel() } #if defined(PLATFORM_OS_WINDOWS) - Gtk::Label *dark_lab = Gtk::manage(GtkUtil::create_label(_("Use dark theme"), false)); - dark_cb = Gtk::manage(new Gtk::CheckButton()); - dark_cb->add(*dark_lab); - panel->add_widget(*dark_cb, false, false); + dark_combo = Gtk::manage(new Gtk::ComboBoxText()); + dark_combo->append(_("Light")); + dark_combo->append(_("Dark")); + dark_combo->append(_("Auto")); + panel->add_label(_("Dark mode:"), *dark_combo); - connector->connect(GUIConfig::theme_dark(), dc::wrap(dark_cb)); + // Block types + + int dark_idx = 0; + switch (GUIConfig::light_dark_mode()()) + { + case LightDarkTheme::Light: + dark_idx = 0; + break; + case LightDarkTheme::Dark: + dark_idx = 1; + break; + case LightDarkTheme::Auto: + dark_idx = 2; + break; + default: + block_idx = 2; + } + dark_combo->set_active(dark_idx); + dark_combo->signal_changed().connect(sigc::mem_fun(*this, &GeneralPreferencePanel::on_dark_changed)); + + connector->connect(GUIConfig::light_dark_mode(), dc::wrap(dark_combo)); #endif pack_start(*panel, false, false, 0); panel->set_border_width(12); } +#if defined(PLATFORM_OS_WINDOWS) void GeneralPreferencePanel::on_block_changed() { int idx = block_button->get_active_row_number(); + LightDarkTheme m; + switch (idx) + { + case 0: + m = LightDarkTheme::Light; + break; + case 1: + m = LightDarkTheme::Dark; + break; + default: + m = LightDarkTheme::Auto; + } + GUIConfig::light_dark_mode().set(m); +} +#endif + +void +GeneralPreferencePanel::on_dark_changed() +{ + int idx = dark_combo->get_active_row_number(); BlockMode m; switch (idx) { @@ -295,7 +337,7 @@ GeneralPreferencePanel::on_native_cell_data(const Gtk::TreeModel::const_iterator } int -GeneralPreferencePanel::on_cell_data_compare(const Gtk::TreeModel::iterator &iter1, const Gtk::TreeModel::iterator &iter2) +GeneralPreferencePanel::on_cell_data_compare(const Gtk::TreeModel::iterator &iter1, const Gtk::TreeModel::iterator &iter2) const { Gtk::TreeModel::Row row1 = *iter1; Gtk::TreeModel::Row row2 = *iter2; diff --git a/ui/app/toolkits/gtkmm/preferences/GeneralPreferencePanel.hh b/ui/app/toolkits/gtkmm/preferences/GeneralPreferencePanel.hh index 3399aa006..d64378241 100644 --- a/ui/app/toolkits/gtkmm/preferences/GeneralPreferencePanel.hh +++ b/ui/app/toolkits/gtkmm/preferences/GeneralPreferencePanel.hh @@ -72,7 +72,7 @@ private: void on_native_cell_data(const Gtk::TreeModel::const_iterator &iter); void on_current_cell_data(const Gtk::TreeModel::const_iterator &iter); - int on_cell_data_compare(const Gtk::TreeModel::iterator &iter1, const Gtk::TreeModel::iterator &iter2); + int on_cell_data_compare(const Gtk::TreeModel::iterator &iter1, const Gtk::TreeModel::iterator &iter2) const; Gtk::ComboBox languages_combo; ModelColumns languages_columns; @@ -86,8 +86,9 @@ private: #endif #if defined(PLATFORM_OS_WINDOWS) - Gtk::CheckButton *dark_cb{nullptr}; + Gtk::ComboBoxText *dark_combo{nullptr}; Glib::RefPtr dark_binding; + void on_dark_changed(); #endif std::shared_ptr general_frame;