mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 19:57:57 +01:00
## Summary of the Pull Request This PR adds a sorting button to the module list so it can be sorted alphabetically (default) or by status. Fixes: #41837 <img width="760" height="933" alt="image" src="https://github.com/user-attachments/assets/69c831a9-ae9e-4849-b630-772d03e4a4b1" /> @yeelam-gordon When running the runner, I do see the settings value is being updated. But when running the runner again the setting doesn't seem to be saved? Is that because of a bug in my implementation, or am I testing it wrong :)? <img width="286" height="86" alt="image" src="https://github.com/user-attachments/assets/3a90997f-c40c-4f50-8361-b1ae3aa02052" /> <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed --------- Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com> Co-authored-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
407 lines
16 KiB
C++
407 lines
16 KiB
C++
#include "pch.h"
|
|
#include "general_settings.h"
|
|
#include "auto_start_helper.h"
|
|
#include "tray_icon.h"
|
|
#include "Generated files/resource.h"
|
|
#include "hotkey_conflict_detector.h"
|
|
|
|
#include <common/SettingsAPI/settings_helpers.h>
|
|
#include "powertoy_module.h"
|
|
#include <common/themes/windows_colors.h>
|
|
|
|
#include "trace.h"
|
|
#include <common/utils/elevation.h>
|
|
#include <common/version/version.h>
|
|
#include <common/utils/resources.h>
|
|
|
|
namespace
|
|
{
|
|
json::JsonValue create_empty_shortcut_array_value()
|
|
{
|
|
return json::JsonValue::Parse(L"[]");
|
|
}
|
|
|
|
void ensure_ignored_conflict_properties_shape(json::JsonObject& obj)
|
|
{
|
|
if (!json::has(obj, L"ignored_shortcuts", json::JsonValueType::Array))
|
|
{
|
|
obj.SetNamedValue(L"ignored_shortcuts", create_empty_shortcut_array_value());
|
|
}
|
|
}
|
|
|
|
json::JsonObject create_default_ignored_conflict_properties()
|
|
{
|
|
json::JsonObject obj;
|
|
ensure_ignored_conflict_properties_shape(obj);
|
|
return obj;
|
|
}
|
|
|
|
DashboardSortOrder parse_dashboard_sort_order(const json::JsonObject& obj, DashboardSortOrder fallback)
|
|
{
|
|
if (json::has(obj, L"dashboard_sort_order", json::JsonValueType::Number))
|
|
{
|
|
const auto raw_value = static_cast<int>(obj.GetNamedNumber(L"dashboard_sort_order", static_cast<double>(static_cast<int>(fallback))));
|
|
return raw_value == static_cast<int>(DashboardSortOrder::ByStatus) ? DashboardSortOrder::ByStatus : DashboardSortOrder::Alphabetical;
|
|
}
|
|
|
|
if (json::has(obj, L"dashboard_sort_order", json::JsonValueType::String))
|
|
{
|
|
const auto raw = obj.GetNamedString(L"dashboard_sort_order");
|
|
if (raw == L"ByStatus")
|
|
{
|
|
return DashboardSortOrder::ByStatus;
|
|
}
|
|
|
|
if (raw == L"Alphabetical")
|
|
{
|
|
return DashboardSortOrder::Alphabetical;
|
|
}
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
// 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 run_as_elevated = false;
|
|
static bool show_new_updates_toast_notification = true;
|
|
static bool download_updates_automatically = true;
|
|
static bool show_whats_new_after_updates = true;
|
|
static bool enable_experimentation = true;
|
|
static bool enable_warnings_elevated_apps = true;
|
|
static DashboardSortOrder dashboard_sort_order = DashboardSortOrder::Alphabetical;
|
|
static json::JsonObject ignored_conflict_properties = create_default_ignored_conflict_properties();
|
|
|
|
json::JsonObject GeneralSettings::to_json()
|
|
{
|
|
json::JsonObject result;
|
|
|
|
auto ignoredProps = ignoredConflictProperties;
|
|
ensure_ignored_conflict_properties_shape(ignoredProps);
|
|
|
|
result.SetNamedValue(L"startup", json::value(isStartupEnabled));
|
|
if (!startupDisabledReason.empty())
|
|
{
|
|
result.SetNamedValue(L"startup_disabled_reason", json::value(startupDisabledReason));
|
|
}
|
|
|
|
json::JsonObject enabled;
|
|
for (const auto& [name, isEnabled] : isModulesEnabledMap)
|
|
{
|
|
enabled.SetNamedValue(name, json::value(isEnabled));
|
|
}
|
|
result.SetNamedValue(L"enabled", std::move(enabled));
|
|
|
|
result.SetNamedValue(L"show_tray_icon", json::value(showSystemTrayIcon));
|
|
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));
|
|
result.SetNamedValue(L"download_updates_automatically", json::value(downloadUpdatesAutomatically));
|
|
result.SetNamedValue(L"show_whats_new_after_updates", json::value(showWhatsNewAfterUpdates));
|
|
result.SetNamedValue(L"enable_experimentation", json::value(enableExperimentation));
|
|
result.SetNamedValue(L"dashboard_sort_order", json::value(static_cast<int>(dashboardSortOrder)));
|
|
result.SetNamedValue(L"is_admin", json::value(isAdmin));
|
|
result.SetNamedValue(L"enable_warnings_elevated_apps", json::value(enableWarningsElevatedApps));
|
|
result.SetNamedValue(L"theme", json::value(theme));
|
|
result.SetNamedValue(L"system_theme", json::value(systemTheme));
|
|
result.SetNamedValue(L"powertoys_version", json::value(powerToysVersion));
|
|
result.SetNamedValue(L"ignored_conflict_properties", json::value(ignoredProps));
|
|
|
|
return result;
|
|
}
|
|
|
|
json::JsonObject load_general_settings()
|
|
{
|
|
auto loaded = PTSettingsHelper::load_general_settings();
|
|
settings_theme = loaded.GetNamedString(L"theme", L"system");
|
|
if (settings_theme != L"dark" && settings_theme != L"light")
|
|
{
|
|
settings_theme = L"system";
|
|
}
|
|
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();
|
|
show_whats_new_after_updates = loaded.GetNamedBoolean(L"show_whats_new_after_updates", true);
|
|
enable_experimentation = loaded.GetNamedBoolean(L"enable_experimentation", true);
|
|
enable_warnings_elevated_apps = loaded.GetNamedBoolean(L"enable_warnings_elevated_apps", true);
|
|
dashboard_sort_order = parse_dashboard_sort_order(loaded, dashboard_sort_order);
|
|
|
|
if (json::has(loaded, L"ignored_conflict_properties", json::JsonValueType::Object))
|
|
{
|
|
ignored_conflict_properties = loaded.GetNamedObject(L"ignored_conflict_properties");
|
|
}
|
|
else
|
|
{
|
|
ignored_conflict_properties = create_default_ignored_conflict_properties();
|
|
}
|
|
|
|
ensure_ignored_conflict_properties_shape(ignored_conflict_properties);
|
|
|
|
return loaded;
|
|
}
|
|
|
|
GeneralSettings get_general_settings()
|
|
{
|
|
const bool is_user_admin = check_user_is_admin();
|
|
GeneralSettings settings
|
|
{
|
|
.showSystemTrayIcon = show_tray_icon,
|
|
.isElevated = is_process_elevated(),
|
|
.isRunElevated = run_as_elevated,
|
|
.isAdmin = is_user_admin,
|
|
.enableWarningsElevatedApps = enable_warnings_elevated_apps,
|
|
.showNewUpdatesToastNotification = show_new_updates_toast_notification,
|
|
.downloadUpdatesAutomatically = download_updates_automatically && is_user_admin,
|
|
.showWhatsNewAfterUpdates = show_whats_new_after_updates,
|
|
.enableExperimentation = enable_experimentation,
|
|
.dashboardSortOrder = dashboard_sort_order,
|
|
.theme = settings_theme,
|
|
.systemTheme = WindowsColors::is_dark_mode() ? L"dark" : L"light",
|
|
.powerToysVersion = get_product_version(),
|
|
.ignoredConflictProperties = ignored_conflict_properties
|
|
};
|
|
|
|
ensure_ignored_conflict_properties_shape(settings.ignoredConflictProperties);
|
|
|
|
settings.isStartupEnabled = is_auto_start_task_active_for_this_user();
|
|
|
|
for (auto& [name, powertoy] : modules())
|
|
{
|
|
settings.isModulesEnabledMap[name] = powertoy->is_enabled();
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
|
|
void apply_general_settings(const json::JsonObject& general_configs, bool save)
|
|
{
|
|
Logger::info(L"apply_general_settings: {}", std::wstring{ general_configs.ToString() });
|
|
run_as_elevated = general_configs.GetNamedBoolean(L"run_elevated", false);
|
|
|
|
enable_warnings_elevated_apps = general_configs.GetNamedBoolean(L"enable_warnings_elevated_apps", true);
|
|
|
|
show_new_updates_toast_notification = general_configs.GetNamedBoolean(L"show_new_updates_toast_notification", true);
|
|
|
|
download_updates_automatically = general_configs.GetNamedBoolean(L"download_updates_automatically", true);
|
|
show_whats_new_after_updates = general_configs.GetNamedBoolean(L"show_whats_new_after_updates", true);
|
|
|
|
enable_experimentation = general_configs.GetNamedBoolean(L"enable_experimentation", true);
|
|
dashboard_sort_order = parse_dashboard_sort_order(general_configs, dashboard_sort_order);
|
|
|
|
// apply_general_settings is called by the runner's WinMain, so we can just force the run at startup gpo rule here.
|
|
auto gpo_run_as_startup = powertoys_gpo::getConfiguredRunAtStartupValue();
|
|
|
|
if (json::has(general_configs, L"startup", json::JsonValueType::Boolean))
|
|
{
|
|
bool startup = general_configs.GetNamedBoolean(L"startup");
|
|
|
|
if (gpo_run_as_startup == powertoys_gpo::gpo_rule_configured_enabled)
|
|
{
|
|
startup = true;
|
|
}
|
|
else if (gpo_run_as_startup == powertoys_gpo::gpo_rule_configured_disabled)
|
|
{
|
|
startup = false;
|
|
}
|
|
|
|
if (startup)
|
|
{
|
|
if (is_process_elevated())
|
|
{
|
|
delete_auto_start_task_for_this_user();
|
|
create_auto_start_task_for_this_user(run_as_elevated);
|
|
}
|
|
else
|
|
{
|
|
if (!is_auto_start_task_active_for_this_user())
|
|
{
|
|
delete_auto_start_task_for_this_user();
|
|
create_auto_start_task_for_this_user(false);
|
|
|
|
run_as_elevated = false;
|
|
}
|
|
else if (!general_configs.GetNamedBoolean(L"run_elevated", false))
|
|
{
|
|
delete_auto_start_task_for_this_user();
|
|
create_auto_start_task_for_this_user(false);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
delete_auto_start_task_for_this_user();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
delete_auto_start_task_for_this_user();
|
|
if (gpo_run_as_startup == powertoys_gpo::gpo_rule_configured_enabled || gpo_run_as_startup == powertoys_gpo::gpo_rule_configured_not_configured)
|
|
{
|
|
create_auto_start_task_for_this_user(run_as_elevated);
|
|
}
|
|
}
|
|
|
|
if (json::has(general_configs, L"enabled"))
|
|
{
|
|
for (const auto& enabled_element : general_configs.GetNamedObject(L"enabled"))
|
|
{
|
|
const auto value = enabled_element.Value();
|
|
if (value.ValueType() != json::JsonValueType::Boolean)
|
|
{
|
|
continue;
|
|
}
|
|
const std::wstring name{ enabled_element.Key().c_str() };
|
|
const bool found = modules().find(name) != modules().end();
|
|
if (!found)
|
|
{
|
|
continue;
|
|
}
|
|
PowertoyModule& powertoy = modules().at(name);
|
|
const bool module_inst_enabled = powertoy->is_enabled();
|
|
bool target_enabled = value.GetBoolean();
|
|
|
|
auto gpo_rule = powertoy->gpo_policy_enabled_configuration();
|
|
if (gpo_rule == powertoys_gpo::gpo_rule_configured_enabled || gpo_rule == powertoys_gpo::gpo_rule_configured_disabled)
|
|
{
|
|
// Apply the GPO Rule.
|
|
target_enabled = gpo_rule == powertoys_gpo::gpo_rule_configured_enabled;
|
|
}
|
|
|
|
if (module_inst_enabled == target_enabled)
|
|
{
|
|
continue;
|
|
}
|
|
if (target_enabled)
|
|
{
|
|
Logger::info(L"apply_general_settings: Enabling powertoy {}", name);
|
|
powertoy->enable();
|
|
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
|
|
hkmng.EnableHotkeyByModule(name);
|
|
}
|
|
else
|
|
{
|
|
Logger::info(L"apply_general_settings: Disabling powertoy {}", name);
|
|
powertoy->disable();
|
|
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
|
|
hkmng.DisableHotkeyByModule(name);
|
|
}
|
|
// Sync the hotkey state with the module state, so it can be removed for disabled modules.
|
|
powertoy.UpdateHotkeyEx();
|
|
}
|
|
}
|
|
|
|
if (json::has(general_configs, L"theme", json::JsonValueType::String))
|
|
{
|
|
settings_theme = general_configs.GetNamedString(L"theme");
|
|
}
|
|
|
|
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"ignored_conflict_properties", json::JsonValueType::Object))
|
|
{
|
|
ignored_conflict_properties = general_configs.GetNamedObject(L"ignored_conflict_properties");
|
|
ensure_ignored_conflict_properties_shape(ignored_conflict_properties);
|
|
}
|
|
|
|
if (save)
|
|
{
|
|
GeneralSettings save_settings = get_general_settings();
|
|
PTSettingsHelper::save_general_settings(save_settings.to_json());
|
|
Trace::SettingsChanged(save_settings);
|
|
}
|
|
}
|
|
|
|
void start_enabled_powertoys()
|
|
{
|
|
std::unordered_set<std::wstring> powertoys_to_disable;
|
|
std::unordered_map<std::wstring, powertoys_gpo::gpo_rule_configured_t> powertoys_gpo_configuration;
|
|
// Take into account default values supplied by modules themselves and gpo configurations
|
|
for (auto& [name, powertoy] : modules())
|
|
{
|
|
auto gpo_rule = powertoy->gpo_policy_enabled_configuration();
|
|
powertoys_gpo_configuration[name] = gpo_rule;
|
|
if (gpo_rule == powertoys_gpo::gpo_rule_configured_unavailable)
|
|
{
|
|
Logger::warn(L"start_enabled_powertoys: couldn't read the gpo rule for Powertoy {}", name);
|
|
}
|
|
if (gpo_rule == powertoys_gpo::gpo_rule_configured_wrong_value)
|
|
{
|
|
Logger::warn(L"start_enabled_powertoys: gpo rule for Powertoy {} is set to an unknown value", name);
|
|
}
|
|
|
|
if (!powertoy->is_enabled_by_default())
|
|
powertoys_to_disable.emplace(name);
|
|
}
|
|
|
|
json::JsonObject general_settings;
|
|
try
|
|
{
|
|
general_settings = load_general_settings();
|
|
if (general_settings.HasKey(L"enabled"))
|
|
{
|
|
json::JsonObject enabled = general_settings.GetNamedObject(L"enabled");
|
|
for (const auto& disabled_element : enabled)
|
|
{
|
|
std::wstring disable_module_name{ static_cast<std::wstring_view>(disabled_element.Key()) };
|
|
|
|
if (powertoys_gpo_configuration.find(disable_module_name) != powertoys_gpo_configuration.end() && (powertoys_gpo_configuration[disable_module_name] == powertoys_gpo::gpo_rule_configured_enabled || powertoys_gpo_configuration[disable_module_name] == powertoys_gpo::gpo_rule_configured_disabled))
|
|
{
|
|
// If gpo forces the enabled setting, no need to check the setting for this PowerToy. It will be applied later on this function.
|
|
continue;
|
|
}
|
|
|
|
// Disable explicitly disabled modules
|
|
if (!disabled_element.Value().GetBoolean())
|
|
{
|
|
Logger::info(L"start_enabled_powertoys: Powertoy {} explicitly disabled", disable_module_name);
|
|
powertoys_to_disable.emplace(std::move(disable_module_name));
|
|
}
|
|
// If module was scheduled for disable, but it's enabled in the settings - override default value
|
|
else if (auto it = powertoys_to_disable.find(disable_module_name); it != end(powertoys_to_disable))
|
|
{
|
|
Logger::info(L"start_enabled_powertoys: Overriding default enabled value for {} powertoy", disable_module_name);
|
|
powertoys_to_disable.erase(it);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
|
|
for (auto& [name, powertoy] : modules())
|
|
{
|
|
bool should_powertoy_be_enabled = true;
|
|
|
|
auto gpo_rule = powertoys_gpo_configuration.find(name) != powertoys_gpo_configuration.end() ? powertoys_gpo_configuration[name] : powertoys_gpo::gpo_rule_configured_not_configured;
|
|
|
|
if (gpo_rule == powertoys_gpo::gpo_rule_configured_enabled || gpo_rule == powertoys_gpo::gpo_rule_configured_disabled)
|
|
{
|
|
// Apply the GPO Rule.
|
|
should_powertoy_be_enabled = gpo_rule == powertoys_gpo::gpo_rule_configured_enabled;
|
|
Logger::info(L"start_enabled_powertoys: GPO sets the enabled state for {} powertoy as {}", name, should_powertoy_be_enabled);
|
|
}
|
|
else if (powertoys_to_disable.contains(name))
|
|
{
|
|
// Apply the settings or default information provided by the PowerToy on first run.
|
|
should_powertoy_be_enabled = false;
|
|
}
|
|
|
|
if (should_powertoy_be_enabled)
|
|
{
|
|
Logger::info(L"start_enabled_powertoys: Enabling powertoy {}", name);
|
|
powertoy->enable();
|
|
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
|
|
hkmng.EnableHotkeyByModule(name);
|
|
powertoy.UpdateHotkeyEx();
|
|
}
|
|
}
|
|
}
|