diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 6cb41322e4..d9e5f7e254 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -364,6 +364,7 @@ DEFAULTFLAGS DEFAULTICON defaultlib DEFAULTONLY +DEFAULTSIZE DEFAULTTONEAREST Defaulttonearest DEFAULTTONULL @@ -830,9 +831,11 @@ ITHUMBNAIL IUI IUWP IWIC +jeli jfif jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi jjw +JOBOBJECT jobject jpe jpnime diff --git a/src/runner/general_settings.cpp b/src/runner/general_settings.cpp index 2ae775a0fc..5024e39753 100644 --- a/src/runner/general_settings.cpp +++ b/src/runner/general_settings.cpp @@ -67,6 +67,7 @@ namespace // TODO: would be nice to get rid of these globals, since they're basically cached json settings static std::wstring settings_theme = L"system"; static bool show_tray_icon = true; +static bool show_theme_adaptive_tray_icon = false; static bool run_as_elevated = false; static bool show_new_updates_toast_notification = true; static bool download_updates_automatically = true; @@ -99,6 +100,7 @@ json::JsonObject GeneralSettings::to_json() result.SetNamedValue(L"enabled", std::move(enabled)); result.SetNamedValue(L"show_tray_icon", json::value(showSystemTrayIcon)); + result.SetNamedValue(L"show_theme_adaptive_tray_icon", json::value(showThemeAdaptiveTrayIcon)); result.SetNamedValue(L"is_elevated", json::value(isElevated)); result.SetNamedValue(L"run_elevated", json::value(isRunElevated)); result.SetNamedValue(L"show_new_updates_toast_notification", json::value(showNewUpdatesToastNotification)); @@ -126,6 +128,8 @@ json::JsonObject load_general_settings() { settings_theme = L"system"; } + show_tray_icon = loaded.GetNamedBoolean(L"show_tray_icon", true); + show_theme_adaptive_tray_icon = loaded.GetNamedBoolean(L"show_theme_adaptive_tray_icon", false); run_as_elevated = loaded.GetNamedBoolean(L"run_elevated", false); show_new_updates_toast_notification = loaded.GetNamedBoolean(L"show_new_updates_toast_notification", true); download_updates_automatically = loaded.GetNamedBoolean(L"download_updates_automatically", true) && check_user_is_admin(); @@ -159,6 +163,7 @@ GeneralSettings get_general_settings() GeneralSettings settings { .showSystemTrayIcon = show_tray_icon, + .showThemeAdaptiveTrayIcon = show_theme_adaptive_tray_icon, .isElevated = is_process_elevated(), .isRunElevated = run_as_elevated, .isAdmin = is_user_admin, @@ -356,10 +361,19 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save) if (json::has(general_configs, L"show_tray_icon", json::JsonValueType::Boolean)) { show_tray_icon = general_configs.GetNamedBoolean(L"show_tray_icon"); - // Update tray icon visibility when setting is toggled set_tray_icon_visible(show_tray_icon); } + if (json::has(general_configs, L"show_theme_adaptive_tray_icon", json::JsonValueType::Boolean)) + { + bool new_theme_adaptive = general_configs.GetNamedBoolean(L"show_theme_adaptive_tray_icon"); + if (show_theme_adaptive_tray_icon != new_theme_adaptive) + { + show_theme_adaptive_tray_icon = new_theme_adaptive; + set_tray_icon_theme_adaptive(show_theme_adaptive_tray_icon); + } + } + if (json::has(general_configs, L"ignored_conflict_properties", json::JsonValueType::Object)) { ignored_conflict_properties = general_configs.GetNamedObject(L"ignored_conflict_properties"); diff --git a/src/runner/general_settings.h b/src/runner/general_settings.h index 033f75b087..ac93a1fdfd 100644 --- a/src/runner/general_settings.h +++ b/src/runner/general_settings.h @@ -13,6 +13,7 @@ struct GeneralSettings { bool isStartupEnabled; bool showSystemTrayIcon; + bool showThemeAdaptiveTrayIcon; std::wstring startupDisabledReason; std::map isModulesEnabledMap; bool isElevated; diff --git a/src/runner/main.cpp b/src/runner/main.cpp index d9da20d93c..d8fdcbdb04 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -189,13 +189,18 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow //init_global_error_handlers(); #endif Trace::RegisterProvider(); - start_tray_icon(isProcessElevated); - if (get_general_settings().enableQuickAccess) + + // Load settings from file before reading them + load_general_settings(); + auto const settings = get_general_settings(); + start_tray_icon(isProcessElevated, settings.showThemeAdaptiveTrayIcon); + + if (settings.enableQuickAccess) { QuickAccessHost::start(); } - update_quick_access_hotkey(get_general_settings().enableQuickAccess, get_general_settings().quickAccessShortcut); - set_tray_icon_visible(get_general_settings().showSystemTrayIcon); + update_quick_access_hotkey(settings.enableQuickAccess, settings.quickAccessShortcut); + set_tray_icon_visible(settings.showSystemTrayIcon); CentralizedKeyboardHook::Start(); int result = -1; diff --git a/src/runner/quick_access_host.cpp b/src/runner/quick_access_host.cpp index 609b8f2f36..b546ee244e 100644 --- a/src/runner/quick_access_host.cpp +++ b/src/runner/quick_access_host.cpp @@ -18,6 +18,7 @@ extern void receive_json_send_to_main_thread(const std::wstring& msg); namespace { wil::unique_handle quick_access_process; + wil::unique_handle quick_access_job; wil::unique_handle show_event; wil::unique_handle exit_event; std::wstring show_event_name; @@ -53,6 +54,7 @@ namespace } quick_access_process.reset(); + quick_access_job.reset(); show_event.reset(); exit_event.reset(); show_event_name.clear(); @@ -206,7 +208,7 @@ namespace QuickAccessHost startup_info.cb = sizeof(startup_info); PROCESS_INFORMATION process_info{}; - BOOL created = CreateProcessW(exe_path.c_str(), command_line_buffer.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup_info, &process_info); + BOOL created = CreateProcessW(exe_path.c_str(), command_line_buffer.data(), nullptr, nullptr, FALSE, CREATE_SUSPENDED, nullptr, nullptr, &startup_info, &process_info); if (!created) { Logger::error(L"QuickAccessHost: failed to launch Quick Access host. error={}.", GetLastError()); @@ -215,6 +217,31 @@ namespace QuickAccessHost } quick_access_process.reset(process_info.hProcess); + + // Assign to job object to ensure the process is killed if the runner exits unexpectedly (e.g. debugging stop) + quick_access_job.reset(CreateJobObjectW(nullptr, nullptr)); + if (quick_access_job) + { + JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = { 0 }; + jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + if (!SetInformationJobObject(quick_access_job.get(), JobObjectExtendedLimitInformation, &jeli, sizeof(jeli))) + { + Logger::warn(L"QuickAccessHost: failed to set job object information. error={}", GetLastError()); + } + else + { + if (!AssignProcessToJobObject(quick_access_job.get(), quick_access_process.get())) + { + Logger::warn(L"QuickAccessHost: failed to assign process to job object. error={}", GetLastError()); + } + } + } + else + { + Logger::warn(L"QuickAccessHost: failed to create job object. error={}", GetLastError()); + } + + ResumeThread(process_info.hThread); CloseHandle(process_info.hThread); } diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj index 1bfd036290..23cc1c9d9f 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -141,6 +141,28 @@ + + + true + true + true + true + $(OutDir)\svgs + $(OutDir)\svgs + $(OutDir)\svgs + $(OutDir)\svgs + + + true + true + true + true + $(OutDir)\svgs + $(OutDir)\svgs + $(OutDir)\svgs + $(OutDir)\svgs + + diff --git a/src/runner/runner.vcxproj.filters b/src/runner/runner.vcxproj.filters index ac5fe3ec36..4d1e82ff0d 100644 --- a/src/runner/runner.vcxproj.filters +++ b/src/runner/runner.vcxproj.filters @@ -122,6 +122,8 @@ + + diff --git a/src/runner/svgs/PowerToysDark.ico b/src/runner/svgs/PowerToysDark.ico new file mode 100644 index 0000000000..313a3d01ec Binary files /dev/null and b/src/runner/svgs/PowerToysDark.ico differ diff --git a/src/runner/svgs/PowerToysWhite.ico b/src/runner/svgs/PowerToysWhite.ico new file mode 100644 index 0000000000..9e55ac8794 Binary files /dev/null and b/src/runner/svgs/PowerToysWhite.ico differ diff --git a/src/runner/tray_icon.cpp b/src/runner/tray_icon.cpp index 92b723c9cb..9cff0c36ff 100644 --- a/src/runner/tray_icon.cpp +++ b/src/runner/tray_icon.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "bug_report.h" namespace @@ -39,6 +40,8 @@ namespace bool double_clicked = false; POINT tray_icon_click_point; + static ThemeListener theme_listener; + static bool theme_adaptive_enabled = false; } // Struct to fill with callback and the data. The window_proc is responsible for cleaning it. @@ -266,6 +269,28 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam return DefWindowProc(window, message, wparam, lparam); } +static HICON get_icon(Theme theme) +{ + std::wstring icon_path = get_module_folderpath(); + icon_path += theme == Theme::Dark ? L"\\svgs\\PowerToysWhite.ico" : L"\\svgs\\PowerToysDark.ico"; + return static_cast(LoadImage(NULL, + icon_path.c_str(), + IMAGE_ICON, + 0, + 0, + LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED)); +} + + +static void handle_theme_change() +{ + if (theme_adaptive_enabled) + { + tray_icon_data.hIcon = get_icon(theme_listener.AppTheme); + Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data); + } +} + void update_bug_report_menu_status(bool isRunning) { if (h_sub_menu != nullptr) @@ -274,10 +299,11 @@ void update_bug_report_menu_status(bool isRunning) } } -void start_tray_icon(bool isProcessElevated) +void start_tray_icon(bool isProcessElevated, bool theme_adaptive) { + theme_adaptive_enabled = theme_adaptive; auto h_instance = reinterpret_cast(&__ImageBase); - auto icon = LoadIcon(h_instance, MAKEINTRESOURCE(APPICON)); + HICON const icon = theme_adaptive ? get_icon(theme_listener.AppTheme) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON)); if (icon) { UINT id_tray_icon = 1; @@ -324,6 +350,7 @@ void start_tray_icon(bool isProcessElevated) ChangeWindowMessageFilterEx(hwnd, WM_COMMAND, MSGFLT_ALLOW, nullptr); tray_icon_created = Shell_NotifyIcon(NIM_ADD, &tray_icon_data) == TRUE; + theme_listener.AddChangedHandler(&handle_theme_change); // Register callback to update bug report menu item status BugReportManager::instance().register_callback([](bool isRunning) { @@ -345,6 +372,18 @@ void set_tray_icon_visible(bool shouldIconBeVisible) Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data); } +void set_tray_icon_theme_adaptive(bool theme_adaptive) +{ + theme_adaptive_enabled = theme_adaptive; + auto h_instance = reinterpret_cast(&__ImageBase); + HICON const icon = theme_adaptive ? get_icon(theme_listener.AppTheme) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON)); + if (icon) + { + tray_icon_data.hIcon = icon; + Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data); + } +} + void stop_tray_icon() { if (tray_icon_created) diff --git a/src/runner/tray_icon.h b/src/runner/tray_icon.h index e94b7630f4..5ef4c3a75b 100644 --- a/src/runner/tray_icon.h +++ b/src/runner/tray_icon.h @@ -4,9 +4,11 @@ #include // Start the Tray Icon -void start_tray_icon(bool isProcessElevated); +void start_tray_icon(bool isProcessElevated, bool theme_adaptive); // Change the Tray Icon visibility void set_tray_icon_visible(bool shouldIconBeVisible); +// Enable or disable theme adaptive tray icon at runtime +void set_tray_icon_theme_adaptive(bool theme_adaptive); // Stop the Tray Icon void stop_tray_icon(); // Open the Settings Window diff --git a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs index b02f28ff13..415eb60040 100644 --- a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs +++ b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs @@ -30,6 +30,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("show_tray_icon")] public bool ShowSysTrayIcon { get; set; } + // Gets or sets a value indicating whether the powertoys system tray icon should show a theme adaptive icon + [JsonPropertyName("show_theme_adaptive_tray_icon")] + public bool ShowThemeAdaptiveTrayIcon { get; set; } + // Gets or sets a value indicating whether the powertoy elevated. [CmdConfigureIgnoreAttribute] [JsonPropertyName("is_elevated")] diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml index fea485e9b4..a0f45625cf 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml @@ -283,13 +283,21 @@ IsOn="{x:Bind ViewModel.Startup, Mode=TwoWay}" /> - + - + + + + + + A modern UI built with Fluent Design Fluent Design is a product name, do not loc + + Show a monochrome icon that matches the Windows theme + Quick Access flyout diff --git a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs index 321afd3dee..3919eb0a78 100644 --- a/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/GeneralViewModel.cs @@ -164,6 +164,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } _showSysTrayIcon = GeneralSettingsConfig.ShowSysTrayIcon; + _showThemeAdaptiveSysTrayIcon = GeneralSettingsConfig.ShowThemeAdaptiveTrayIcon; _showNewUpdatesToastNotification = GeneralSettingsConfig.ShowNewUpdatesToastNotification; _autoDownloadUpdates = GeneralSettingsConfig.AutoDownloadUpdates; _showWhatsNewAfterUpdates = GeneralSettingsConfig.ShowWhatsNewAfterUpdates; @@ -253,6 +254,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private static bool _isDevBuild; private bool _startup; private bool _showSysTrayIcon; + private bool _showThemeAdaptiveSysTrayIcon; private GpoRuleConfigured _runAtStartupGpoRuleConfiguration; private bool _runAtStartupIsGPOConfigured; private bool _isElevated; @@ -406,6 +408,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public bool ShowThemeAdaptiveTrayIcon + { + get + { + return _showThemeAdaptiveSysTrayIcon; + } + + set + { + if (_showThemeAdaptiveSysTrayIcon != value) + { + _showThemeAdaptiveSysTrayIcon = value; + GeneralSettingsConfig.ShowThemeAdaptiveTrayIcon = value; + NotifyPropertyChanged(); + } + } + } + public string RunningAsText { get