diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 91df199e2c..94e32fc8ee 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -365,6 +365,7 @@ DEFAULTICON defaultlib DEFAULTONLY DEFAULTTONEAREST +Defaulttonearest DEFAULTTONULL DEFAULTTOPRIMARY DEFERERASE @@ -868,6 +869,7 @@ lastcodeanalysissucceeded LASTEXITCODE LAYOUTRTL lbl +Lbuttondown LCh lcid LCIDTo @@ -990,6 +992,7 @@ maxversiontested mber MBM MBR +Mbuttondown MDICHILD MDL mdtext @@ -1448,6 +1451,7 @@ RAWINPUTHEADER RAWMODE RAWPATH rbhid +Rbuttondown rclsid RCZOOMIT remotedesktop @@ -1753,6 +1757,7 @@ svgz SVSI SWFO SWP +Swp SWPNOSIZE SWPNOZORDER SWRESTORE @@ -1772,6 +1777,7 @@ syskeydown SYSKEYUP SYSLIB SYSMENU +Sysmenu systemai SYSTEMAPPS SYSTEMMODAL @@ -2097,6 +2103,7 @@ Wwanpp xap XAxis XButton +Xbuttondown xclip xcopy XDeployment diff --git a/PowerToys.slnx b/PowerToys.slnx index ac006fdbf4..70786b58fb 100644 --- a/PowerToys.slnx +++ b/PowerToys.slnx @@ -1005,6 +1005,14 @@ + + + + + + + + diff --git a/src/common/ManagedCommon/ModuleType.cs b/src/common/ManagedCommon/ModuleType.cs index d7ae386191..8461b4a6d8 100644 --- a/src/common/ManagedCommon/ModuleType.cs +++ b/src/common/ManagedCommon/ModuleType.cs @@ -36,5 +36,6 @@ namespace ManagedCommon PowerOCR, Workspaces, ZoomIt, + GeneralSettings, } } diff --git a/src/common/SettingsAPI/settings_objects.h b/src/common/SettingsAPI/settings_objects.h index 84b064d5af..8927ba5657 100644 --- a/src/common/SettingsAPI/settings_objects.h +++ b/src/common/SettingsAPI/settings_objects.h @@ -119,6 +119,16 @@ namespace PowerToysSettings class HotkeyObject { public: + HotkeyObject() : + m_json(json::JsonObject()) + { + m_json.SetNamedValue(L"win", json::value(false)); + m_json.SetNamedValue(L"ctrl", json::value(false)); + m_json.SetNamedValue(L"alt", json::value(false)); + m_json.SetNamedValue(L"shift", json::value(false)); + m_json.SetNamedValue(L"code", json::value(0)); + m_json.SetNamedValue(L"key", json::value(L"")); + } static HotkeyObject from_json(json::JsonObject json) { return HotkeyObject(std::move(json)); diff --git a/src/runner/general_settings.cpp b/src/runner/general_settings.cpp index c6770731a6..2ae775a0fc 100644 --- a/src/runner/general_settings.cpp +++ b/src/runner/general_settings.cpp @@ -2,6 +2,7 @@ #include "general_settings.h" #include "auto_start_helper.h" #include "tray_icon.h" +#include "quick_access_host.h" #include "Generated files/resource.h" #include "hotkey_conflict_detector.h" @@ -72,6 +73,8 @@ 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 bool enable_quick_access = true; +static PowerToysSettings::HotkeyObject quick_access_shortcut; static DashboardSortOrder dashboard_sort_order = DashboardSortOrder::Alphabetical; static json::JsonObject ignored_conflict_properties = create_default_ignored_conflict_properties(); @@ -105,6 +108,8 @@ json::JsonObject GeneralSettings::to_json() result.SetNamedValue(L"dashboard_sort_order", json::value(static_cast(dashboardSortOrder))); result.SetNamedValue(L"is_admin", json::value(isAdmin)); result.SetNamedValue(L"enable_warnings_elevated_apps", json::value(enableWarningsElevatedApps)); + result.SetNamedValue(L"enable_quick_access", json::value(enableQuickAccess)); + result.SetNamedValue(L"quick_access_shortcut", quickAccessShortcut.get_json()); result.SetNamedValue(L"theme", json::value(theme)); result.SetNamedValue(L"system_theme", json::value(systemTheme)); result.SetNamedValue(L"powertoys_version", json::value(powerToysVersion)); @@ -127,6 +132,11 @@ json::JsonObject load_general_settings() 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); + enable_quick_access = loaded.GetNamedBoolean(L"enable_quick_access", true); + if (json::has(loaded, L"quick_access_shortcut", json::JsonValueType::Object)) + { + quick_access_shortcut = PowerToysSettings::HotkeyObject::from_json(loaded.GetNamedObject(L"quick_access_shortcut")); + } dashboard_sort_order = parse_dashboard_sort_order(loaded, dashboard_sort_order); if (json::has(loaded, L"ignored_conflict_properties", json::JsonValueType::Object)) @@ -153,6 +163,8 @@ GeneralSettings get_general_settings() .isRunElevated = run_as_elevated, .isAdmin = is_user_admin, .enableWarningsElevatedApps = enable_warnings_elevated_apps, + .enableQuickAccess = enable_quick_access, + .quickAccessShortcut = quick_access_shortcut, .showNewUpdatesToastNotification = show_new_updates_toast_notification, .downloadUpdatesAutomatically = download_updates_automatically && is_user_admin, .showWhatsNewAfterUpdates = show_whats_new_after_updates, @@ -178,11 +190,47 @@ GeneralSettings get_general_settings() void apply_general_settings(const json::JsonObject& general_configs, bool save) { + std::wstring old_settings_json_string; + if (save) + { + old_settings_json_string = get_general_settings().to_json().Stringify().c_str(); + } + 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); + bool new_enable_quick_access = general_configs.GetNamedBoolean(L"enable_quick_access", true); + Logger::info(L"apply_general_settings: enable_quick_access={}, new_enable_quick_access={}", enable_quick_access, new_enable_quick_access); + + PowerToysSettings::HotkeyObject new_quick_access_shortcut; + if (json::has(general_configs, L"quick_access_shortcut", json::JsonValueType::Object)) + { + new_quick_access_shortcut = PowerToysSettings::HotkeyObject::from_json(general_configs.GetNamedObject(L"quick_access_shortcut")); + } + + auto hotkey_equals = [](const PowerToysSettings::HotkeyObject& a, const PowerToysSettings::HotkeyObject& b) { + return a.get_code() == b.get_code() && + a.get_modifiers() == b.get_modifiers(); + }; + + if (enable_quick_access != new_enable_quick_access || !hotkey_equals(quick_access_shortcut, new_quick_access_shortcut)) + { + enable_quick_access = new_enable_quick_access; + quick_access_shortcut = new_quick_access_shortcut; + + if (enable_quick_access) + { + QuickAccessHost::start(); + } + else + { + QuickAccessHost::stop(); + } + update_quick_access_hotkey(enable_quick_access, quick_access_shortcut); + } + 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); @@ -321,8 +369,12 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save) if (save) { GeneralSettings save_settings = get_general_settings(); - PTSettingsHelper::save_general_settings(save_settings.to_json()); - Trace::SettingsChanged(save_settings); + std::wstring new_settings_json_string = save_settings.to_json().Stringify().c_str(); + if (old_settings_json_string != new_settings_json_string) + { + PTSettingsHelper::save_general_settings(save_settings.to_json()); + Trace::SettingsChanged(save_settings); + } } } @@ -412,3 +464,5 @@ void start_enabled_powertoys() } } } + + diff --git a/src/runner/general_settings.h b/src/runner/general_settings.h index b4f7638846..033f75b087 100644 --- a/src/runner/general_settings.h +++ b/src/runner/general_settings.h @@ -1,6 +1,7 @@ #pragma once #include +#include enum class DashboardSortOrder { @@ -18,6 +19,8 @@ struct GeneralSettings bool isRunElevated; bool isAdmin; bool enableWarningsElevatedApps; + bool enableQuickAccess; + PowerToysSettings::HotkeyObject quickAccessShortcut; bool showNewUpdatesToastNotification; bool downloadUpdatesAutomatically; bool showWhatsNewAfterUpdates; diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 2ceba89161..d9da20d93c 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -37,6 +37,7 @@ #include #include "centralized_kb_hook.h" #include "centralized_hotkeys.h" +#include "quick_access_host.h" #include "ai_detection.h" #include @@ -189,6 +190,11 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow #endif Trace::RegisterProvider(); start_tray_icon(isProcessElevated); + if (get_general_settings().enableQuickAccess) + { + QuickAccessHost::start(); + } + update_quick_access_hotkey(get_general_settings().enableQuickAccess, get_general_settings().quickAccessShortcut); set_tray_icon_visible(get_general_settings().showSystemTrayIcon); CentralizedKeyboardHook::Start(); @@ -316,7 +322,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow { window = winrt::to_hstring(settingsWindow); } - open_settings_window(window, false); + open_settings_window(window); } if (openOobe) @@ -339,6 +345,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow result = -1; } Trace::UnregisterProvider(); + QuickAccessHost::stop(); return result; } diff --git a/src/runner/quick_access_host.cpp b/src/runner/quick_access_host.cpp new file mode 100644 index 0000000000..609b8f2f36 --- /dev/null +++ b/src/runner/quick_access_host.cpp @@ -0,0 +1,269 @@ +#include "pch.h" +#include "quick_access_host.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +extern void receive_json_send_to_main_thread(const std::wstring& msg); + +namespace +{ + wil::unique_handle quick_access_process; + wil::unique_handle show_event; + wil::unique_handle exit_event; + std::wstring show_event_name; + std::wstring exit_event_name; + std::wstring runner_pipe_name; + std::wstring app_pipe_name; + std::unique_ptr quick_access_ipc; + std::mutex quick_access_mutex; + + bool is_process_active_locked() + { + if (!quick_access_process) + { + return false; + } + + DWORD exit_code = 0; + if (!GetExitCodeProcess(quick_access_process.get(), &exit_code)) + { + Logger::warn(L"QuickAccessHost: failed to read Quick Access process exit code. error={}.", GetLastError()); + return false; + } + + return exit_code == STILL_ACTIVE; + } + + void reset_state_locked() + { + if (quick_access_ipc) + { + quick_access_ipc->end(); + quick_access_ipc.reset(); + } + + quick_access_process.reset(); + show_event.reset(); + exit_event.reset(); + show_event_name.clear(); + exit_event_name.clear(); + runner_pipe_name.clear(); + app_pipe_name.clear(); + } + + std::wstring build_event_name(const wchar_t* suffix) + { + std::wstring name = L"Local\\PowerToysQuickAccess_"; + name += std::to_wstring(GetCurrentProcessId()); + if (suffix) + { + name += suffix; + } + return name; + } + + std::wstring build_command_line(const std::wstring& exe_path) + { + std::wstring command_line = L"\""; + command_line += exe_path; + command_line += L"\" --show-event=\""; + command_line += show_event_name; + command_line += L"\" --exit-event=\""; + command_line += exit_event_name; + command_line += L"\""; + if (!runner_pipe_name.empty()) + { + command_line.append(L" --runner-pipe=\""); + command_line += runner_pipe_name; + command_line += L"\""; + } + if (!app_pipe_name.empty()) + { + command_line.append(L" --app-pipe=\""); + command_line += app_pipe_name; + command_line += L"\""; + } + return command_line; + } +} + +namespace QuickAccessHost +{ + bool is_running() + { + std::scoped_lock lock(quick_access_mutex); + return is_process_active_locked(); + } + + void start() + { + Logger::info(L"QuickAccessHost::start() called"); + std::scoped_lock lock(quick_access_mutex); + if (is_process_active_locked()) + { + Logger::info(L"QuickAccessHost::start: process already active"); + return; + } + + reset_state_locked(); + + show_event_name = build_event_name(L"_Show"); + exit_event_name = build_event_name(L"_Exit"); + + show_event.reset(CreateEventW(nullptr, FALSE, FALSE, show_event_name.c_str())); + if (!show_event) + { + Logger::error(L"QuickAccessHost: failed to create show event. error={}.", GetLastError()); + reset_state_locked(); + return; + } + + exit_event.reset(CreateEventW(nullptr, FALSE, FALSE, exit_event_name.c_str())); + if (!exit_event) + { + Logger::error(L"QuickAccessHost: failed to create exit event. error={}.", GetLastError()); + reset_state_locked(); + return; + } + + runner_pipe_name = L"\\\\.\\pipe\\powertoys_quick_access_runner_"; + app_pipe_name = L"\\\\.\\pipe\\powertoys_quick_access_ui_"; + UUID temp_uuid; + wchar_t* uuid_chars = nullptr; + if (UuidCreate(&temp_uuid) == RPC_S_UUID_NO_ADDRESS) + { + Logger::warn(L"QuickAccessHost: failed to create UUID for pipe names. error={}.", GetLastError()); + } + else if (UuidToString(&temp_uuid, reinterpret_cast(&uuid_chars)) != RPC_S_OK) + { + Logger::warn(L"QuickAccessHost: failed to convert UUID to string. error={}.", GetLastError()); + } + + if (uuid_chars != nullptr) + { + runner_pipe_name += std::wstring(uuid_chars); + app_pipe_name += std::wstring(uuid_chars); + RpcStringFree(reinterpret_cast(&uuid_chars)); + uuid_chars = nullptr; + } + else + { + const std::wstring fallback_suffix = std::to_wstring(GetTickCount64()); + runner_pipe_name += fallback_suffix; + app_pipe_name += fallback_suffix; + } + + HANDLE token_handle = nullptr; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token_handle)) + { + Logger::error(L"QuickAccessHost: failed to open process token. error={}.", GetLastError()); + reset_state_locked(); + return; + } + + wil::unique_handle token(token_handle); + quick_access_ipc.reset(new (std::nothrow) TwoWayPipeMessageIPC(runner_pipe_name, app_pipe_name, receive_json_send_to_main_thread)); + if (!quick_access_ipc) + { + Logger::error(L"QuickAccessHost: failed to allocate IPC instance."); + reset_state_locked(); + return; + } + + try + { + quick_access_ipc->start(token.get()); + } + catch (...) + { + Logger::error(L"QuickAccessHost: failed to start IPC server for Quick Access."); + reset_state_locked(); + return; + } + + const std::wstring exe_path = get_module_folderpath() + L"\\WinUI3Apps\\PowerToys.QuickAccess.exe"; + if (GetFileAttributesW(exe_path.c_str()) == INVALID_FILE_ATTRIBUTES) + { + Logger::warn(L"QuickAccessHost: missing Quick Access executable at {}", exe_path); + reset_state_locked(); + return; + } + + const std::wstring command_line = build_command_line(exe_path); + std::vector command_line_buffer(command_line.begin(), command_line.end()); + command_line_buffer.push_back(L'\0'); + STARTUPINFOW startup_info{}; + 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); + if (!created) + { + Logger::error(L"QuickAccessHost: failed to launch Quick Access host. error={}.", GetLastError()); + reset_state_locked(); + return; + } + + quick_access_process.reset(process_info.hProcess); + CloseHandle(process_info.hThread); + } + + void show() + { + start(); + std::scoped_lock lock(quick_access_mutex); + + if (show_event) + { + if (!SetEvent(show_event.get())) + { + Logger::warn(L"QuickAccessHost: failed to signal show event. error={}.", GetLastError()); + } + } + } + + void stop() + { + Logger::info(L"QuickAccessHost::stop() called"); + std::unique_lock lock(quick_access_mutex); + if (exit_event) + { + SetEvent(exit_event.get()); + } + + if (quick_access_process) + { + const DWORD wait_result = WaitForSingleObject(quick_access_process.get(), 2000); + Logger::info(L"QuickAccessHost::stop: WaitForSingleObject result={}", wait_result); + if (wait_result == WAIT_TIMEOUT) + { + Logger::warn(L"QuickAccessHost: Quick Access process did not exit in time, terminating."); + if (!TerminateProcess(quick_access_process.get(), 0)) + { + Logger::error(L"QuickAccessHost: failed to terminate Quick Access process. error={}.", GetLastError()); + } + else + { + Logger::info(L"QuickAccessHost: TerminateProcess succeeded."); + WaitForSingleObject(quick_access_process.get(), 5000); + } + } + else if (wait_result == WAIT_FAILED) + { + Logger::error(L"QuickAccessHost: failed while waiting for Quick Access process. error={}.", GetLastError()); + } + } + + reset_state_locked(); + } +} diff --git a/src/runner/quick_access_host.h b/src/runner/quick_access_host.h new file mode 100644 index 0000000000..22a65a9c26 --- /dev/null +++ b/src/runner/quick_access_host.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +namespace QuickAccessHost +{ + void start(); + void show(); + void stop(); + bool is_running(); +} diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj index 57cb55b6bd..1bfd036290 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -70,6 +70,7 @@ + @@ -85,6 +86,7 @@ + diff --git a/src/runner/runner.vcxproj.filters b/src/runner/runner.vcxproj.filters index 904e213405..ac5fe3ec36 100644 --- a/src/runner/runner.vcxproj.filters +++ b/src/runner/runner.vcxproj.filters @@ -48,6 +48,9 @@ Utils + + Utils + @@ -102,6 +105,9 @@ Utils + + Utils + diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp index e31b934d40..a1ec3f81b9 100644 --- a/src/runner/settings_window.cpp +++ b/src/runner/settings_window.cpp @@ -198,6 +198,8 @@ void dispatch_received_json(const std::wstring& json_to_parse) return; } + Logger::info(L"dispatch_received_json: {}", json_to_parse); + for (const auto& base_element : j) { const auto name = base_element.Key(); @@ -206,12 +208,12 @@ void dispatch_received_json(const std::wstring& json_to_parse) if (name == L"general") { apply_general_settings(value.GetObjectW()); - const std::wstring settings_string{ get_all_settings().Stringify().c_str() }; - { - std::unique_lock lock{ ipc_mutex }; - if (current_settings_ipc) - current_settings_ipc->send(settings_string); - } + // const std::wstring settings_string{ get_all_settings().Stringify().c_str() }; + // { + // std::unique_lock lock{ ipc_mutex }; + // if (current_settings_ipc) + // current_settings_ipc->send(settings_string); + // } } else if (name == L"powertoys") { @@ -421,7 +423,7 @@ BOOL run_settings_non_elevated(LPCWSTR executable_path, LPWSTR executable_args, DWORD g_settings_process_id = 0; -void run_settings_window(bool show_oobe_window, bool show_scoobe_window, std::optional settings_window, bool show_flyout = false, const std::optional& flyout_position = std::nullopt) +void run_settings_window(bool show_oobe_window, bool show_scoobe_window, std::optional settings_window) { g_isLaunchInProgress = true; @@ -491,22 +493,16 @@ void run_settings_window(bool show_oobe_window, bool show_scoobe_window, std::op // Arg 9: should scoobe window be shown std::wstring settings_showScoobe = show_scoobe_window ? L"true" : L"false"; - // Arg 10: should flyout be shown - std::wstring settings_showFlyout = show_flyout ? L"true" : L"false"; - - // Arg 11: contains if there's a settings window argument. If true, will add one extra argument with the value to the call. + // Arg 10: contains if there's a settings window argument. If true, will add one extra argument with the value to the call. std::wstring settings_containsSettingsWindow = settings_window.has_value() ? L"true" : L"false"; - // Arg 12: contains if there's flyout coordinates. If true, will add two extra arguments to the call containing the x and y coordinates. - std::wstring settings_containsFlyoutPosition = flyout_position.has_value() ? L"true" : L"false"; - - // Args 13, .... : Optional arguments depending on the options presented before. All by the same value. + // Args 11, .... : Optional arguments depending on the options presented before. All by the same value. // create general settings file to initialize the settings file with installation configurations like : // 1. Run on start up. PTSettingsHelper::save_general_settings(save_settings.to_json()); - std::wstring executable_args = fmt::format(L"\"{}\" {} {} {} {} {} {} {} {} {} {} {}", + std::wstring executable_args = fmt::format(L"\"{}\" {} {} {} {} {} {} {} {} {}", executable_path, powertoys_pipe_name, settings_pipe_name, @@ -516,9 +512,7 @@ void run_settings_window(bool show_oobe_window, bool show_scoobe_window, std::op settings_isUserAnAdmin, settings_showOobe, settings_showScoobe, - settings_showFlyout, - settings_containsSettingsWindow, - settings_containsFlyoutPosition); + settings_containsSettingsWindow); if (settings_window.has_value()) { @@ -526,14 +520,6 @@ void run_settings_window(bool show_oobe_window, bool show_scoobe_window, std::op executable_args.append(settings_window.value()); } - if (flyout_position) - { - executable_args.append(L" "); - executable_args.append(std::to_wstring(flyout_position.value().x)); - executable_args.append(L" "); - executable_args.append(std::to_wstring(flyout_position.value().y)); - } - BOOL process_created = false; // Commented out to fix #22659 @@ -684,39 +670,22 @@ void bring_settings_to_front() EnumWindows(callback, 0); } -void open_settings_window(std::optional settings_window, bool show_flyout = false, const std::optional& flyout_position) +void open_settings_window(std::optional settings_window) { if (g_settings_process_id != 0) { - if (show_flyout) + // nl instead of showing the window, send message to it (flyout might need to be hidden, main setting window activated) + // bring_settings_to_front(); + if (current_settings_ipc) { - if (current_settings_ipc) + if (settings_window.has_value()) { - if (!flyout_position.has_value()) - { - current_settings_ipc->send(L"{\"ShowYourself\":\"flyout\"}"); - } - else - { - current_settings_ipc->send(fmt::format(L"{{\"ShowYourself\":\"flyout\", \"x_position\":{}, \"y_position\":{} }}", std::to_wstring(flyout_position.value().x), std::to_wstring(flyout_position.value().y))); - } + std::wstring msg = L"{\"ShowYourself\":\"" + settings_window.value() + L"\"}"; + current_settings_ipc->send(msg); } - } - else - { - // nl instead of showing the window, send message to it (flyout might need to be hidden, main setting window activated) - // bring_settings_to_front(); - if (current_settings_ipc) + else { - if (settings_window.has_value()) - { - std::wstring msg = L"{\"ShowYourself\":\"" + settings_window.value() + L"\"}"; - current_settings_ipc->send(msg); - } - else - { - current_settings_ipc->send(L"{\"ShowYourself\":\"Dashboard\"}"); - } + current_settings_ipc->send(L"{\"ShowYourself\":\"Dashboard\"}"); } } } @@ -724,8 +693,8 @@ void open_settings_window(std::optional settings_window, bool show { if (!g_isLaunchInProgress) { - std::thread([settings_window, show_flyout, flyout_position]() { - run_settings_window(false, false, settings_window, show_flyout, flyout_position); + std::thread([settings_window]() { + run_settings_window(false, false, settings_window); }).detach(); } } diff --git a/src/runner/settings_window.h b/src/runner/settings_window.h index e15108059f..507d1c65b4 100644 --- a/src/runner/settings_window.h +++ b/src/runner/settings_window.h @@ -41,9 +41,8 @@ enum class ESettingsWindowNames std::string ESettingsWindowNames_to_string(ESettingsWindowNames value); ESettingsWindowNames ESettingsWindowNames_from_string(std::string value); -void open_settings_window(std::optional settings_window, bool show_flyout, const std::optional& flyout_position); +void open_settings_window(std::optional settings_window); void close_settings_window(); void open_oobe_window(); void open_scoobe_window(); -void open_flyout(); diff --git a/src/runner/tray_icon.cpp b/src/runner/tray_icon.cpp index 749c921659..92b723c9cb 100644 --- a/src/runner/tray_icon.cpp +++ b/src/runner/tray_icon.cpp @@ -5,6 +5,8 @@ #include "general_settings.h" #include "centralized_hotkeys.h" #include "centralized_kb_hook.h" +#include "quick_access_host.h" +#include "hotkey_conflict_detector.h" #include #include @@ -69,9 +71,9 @@ void change_menu_item_text(const UINT item_id, wchar_t* new_text) SetMenuItemInfoW(h_menu, item_id, false, &menuitem); } -void open_quick_access_flyout_window(const POINT flyout_position) +void open_quick_access_flyout_window() { - open_settings_window(std::nullopt, true, flyout_position); + QuickAccessHost::show(); } void handle_tray_command(HWND window, const WPARAM command_id, LPARAM lparam) @@ -81,7 +83,7 @@ void handle_tray_command(HWND window, const WPARAM command_id, LPARAM lparam) case ID_SETTINGS_MENU_COMMAND: { std::wstring settings_window{ winrt::to_hstring(ESettingsWindowNames_to_string(static_cast(lparam))) }; - open_settings_window(settings_window, false); + open_settings_window(settings_window); } break; case ID_CLOSE_MENU_COMMAND: @@ -113,9 +115,7 @@ void handle_tray_command(HWND window, const WPARAM command_id, LPARAM lparam) } case ID_QUICK_ACCESS_MENU_COMMAND: { - POINT mouse_pointer; - GetCursorPos(&mouse_pointer); - open_quick_access_flyout_window(mouse_pointer); + open_quick_access_flyout_window(); break; } } @@ -126,7 +126,14 @@ void click_timer_elapsed() double_click_timer_running = false; if (!double_clicked) { - open_quick_access_flyout_window(tray_icon_click_point); + if (get_general_settings().enableQuickAccess) + { + open_quick_access_flyout_window(); + } + else + { + open_settings_window(std::nullopt); + } } } @@ -218,9 +225,6 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam // ignore event if this is the second click of a double click if (!double_click_timer_running) { - // save the cursor position for sending where to show the popup. - GetCursorPos(&tray_icon_click_point); - // start timer for detecting single or double click double_click_timer_running = true; double_clicked = false; @@ -236,7 +240,7 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam case WM_LBUTTONDBLCLK: { double_clicked = true; - open_settings_window(std::nullopt, false); + open_settings_window(std::nullopt); break; } break; @@ -349,4 +353,37 @@ void stop_tray_icon() BugReportManager::instance().clear_callbacks(); SendMessage(tray_icon_hwnd, WM_CLOSE, 0, 0); } -} \ No newline at end of file +} +void update_quick_access_hotkey(bool enabled, PowerToysSettings::HotkeyObject hotkey) +{ + static PowerToysSettings::HotkeyObject current_hotkey; + static bool is_registered = false; + auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance(); + + if (is_registered) + { + CentralizedKeyboardHook::ClearModuleHotkeys(L"QuickAccess"); + hkmng.RemoveHotkeyByModule(L"GeneralSettings"); + is_registered = false; + } + + if (enabled && hotkey.get_code() != 0) + { + HotkeyConflictDetector::Hotkey hk = { + hotkey.win_pressed(), + hotkey.ctrl_pressed(), + hotkey.shift_pressed(), + hotkey.alt_pressed(), + static_cast(hotkey.get_code()) + }; + + hkmng.AddHotkey(hk, L"GeneralSettings", 0, true); + CentralizedKeyboardHook::SetHotkeyAction(L"QuickAccess", hk, []() { + open_quick_access_flyout_window(); + return true; + }); + + current_hotkey = hotkey; + is_registered = true; + } +} diff --git a/src/runner/tray_icon.h b/src/runner/tray_icon.h index 4fa7ebfe5a..e94b7630f4 100644 --- a/src/runner/tray_icon.h +++ b/src/runner/tray_icon.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include // Start the Tray Icon void start_tray_icon(bool isProcessElevated); @@ -9,7 +10,9 @@ void set_tray_icon_visible(bool shouldIconBeVisible); // Stop the Tray Icon void stop_tray_icon(); // Open the Settings Window -void open_settings_window(std::optional settings_window, bool show_flyout, const std::optional& flyout_position = std::nullopt); +void open_settings_window(std::optional settings_window); +// Update Quick Access Hotkey +void update_quick_access_hotkey(bool enabled, PowerToysSettings::HotkeyObject hotkey); // Callback type to be called by the tray icon loop typedef void (*main_loop_callback_function)(PVOID); // Calls a callback in _callback diff --git a/src/settings-ui/QuickAccess.UI/Helpers/ModuleGpoHelper.cs b/src/settings-ui/QuickAccess.UI/Helpers/ModuleGpoHelper.cs new file mode 100644 index 0000000000..25f32e191b --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/Helpers/ModuleGpoHelper.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using global::PowerToys.GPOWrapper; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; + +namespace Microsoft.PowerToys.QuickAccess.Helpers; + +internal static class ModuleGpoHelper +{ + public static GpoRuleConfigured GetModuleGpoConfiguration(ModuleType moduleType) + { + return moduleType switch + { + ModuleType.AdvancedPaste => GPOWrapper.GetConfiguredAdvancedPasteEnabledValue(), + ModuleType.AlwaysOnTop => GPOWrapper.GetConfiguredAlwaysOnTopEnabledValue(), + ModuleType.Awake => GPOWrapper.GetConfiguredAwakeEnabledValue(), + ModuleType.CmdPal => GPOWrapper.GetConfiguredCmdPalEnabledValue(), + ModuleType.ColorPicker => GPOWrapper.GetConfiguredColorPickerEnabledValue(), + ModuleType.CropAndLock => GPOWrapper.GetConfiguredCropAndLockEnabledValue(), + ModuleType.CursorWrap => GPOWrapper.GetConfiguredCursorWrapEnabledValue(), + ModuleType.EnvironmentVariables => GPOWrapper.GetConfiguredEnvironmentVariablesEnabledValue(), + ModuleType.FancyZones => GPOWrapper.GetConfiguredFancyZonesEnabledValue(), + ModuleType.FileLocksmith => GPOWrapper.GetConfiguredFileLocksmithEnabledValue(), + ModuleType.FindMyMouse => GPOWrapper.GetConfiguredFindMyMouseEnabledValue(), + ModuleType.Hosts => GPOWrapper.GetConfiguredHostsFileEditorEnabledValue(), + ModuleType.ImageResizer => GPOWrapper.GetConfiguredImageResizerEnabledValue(), + ModuleType.KeyboardManager => GPOWrapper.GetConfiguredKeyboardManagerEnabledValue(), + ModuleType.MouseHighlighter => GPOWrapper.GetConfiguredMouseHighlighterEnabledValue(), + ModuleType.MouseJump => GPOWrapper.GetConfiguredMouseJumpEnabledValue(), + ModuleType.MousePointerCrosshairs => GPOWrapper.GetConfiguredMousePointerCrosshairsEnabledValue(), + ModuleType.MouseWithoutBorders => GPOWrapper.GetConfiguredMouseWithoutBordersEnabledValue(), + ModuleType.NewPlus => GPOWrapper.GetConfiguredNewPlusEnabledValue(), + ModuleType.Peek => GPOWrapper.GetConfiguredPeekEnabledValue(), + ModuleType.PowerRename => GPOWrapper.GetConfiguredPowerRenameEnabledValue(), + ModuleType.PowerLauncher => GPOWrapper.GetConfiguredPowerLauncherEnabledValue(), + ModuleType.PowerAccent => GPOWrapper.GetConfiguredQuickAccentEnabledValue(), + ModuleType.Workspaces => GPOWrapper.GetConfiguredWorkspacesEnabledValue(), + ModuleType.RegistryPreview => GPOWrapper.GetConfiguredRegistryPreviewEnabledValue(), + ModuleType.MeasureTool => GPOWrapper.GetConfiguredScreenRulerEnabledValue(), + ModuleType.ShortcutGuide => GPOWrapper.GetConfiguredShortcutGuideEnabledValue(), + ModuleType.PowerOCR => GPOWrapper.GetConfiguredTextExtractorEnabledValue(), + ModuleType.ZoomIt => GPOWrapper.GetConfiguredZoomItEnabledValue(), + _ => GpoRuleConfigured.Unavailable, + }; + } +} diff --git a/src/settings-ui/QuickAccess.UI/Helpers/ResourceLoaderInstance.cs b/src/settings-ui/QuickAccess.UI/Helpers/ResourceLoaderInstance.cs new file mode 100644 index 0000000000..b57d73015b --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/Helpers/ResourceLoaderInstance.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Windows.ApplicationModel.Resources; + +namespace Microsoft.PowerToys.QuickAccess.Helpers; + +internal static class ResourceLoaderInstance +{ + internal static ResourceLoader ResourceLoader { get; } = new("PowerToys.QuickAccess.pri"); +} diff --git a/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj b/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj new file mode 100644 index 0000000000..0c20d4d234 --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj @@ -0,0 +1,89 @@ + + + + + + WinExe + net9.0-windows10.0.26100.0 + Microsoft.PowerToys.QuickAccess + PowerToys.QuickAccess + true + None + true + true + false + false + app.manifest + ..\..\..\$(Platform)\$(Configuration)\WinUI3Apps + false + false + enable + PowerToys.QuickAccess.pri + + + + PowerToys.GPOWrapper + $(OutDir) + + + + + + + + + + + + + Resources\Styles\Button.xaml + + + Resources\Styles\TextBlock.xaml + + + Resources\Themes\Colors.xaml + + + Resources\Themes\Generic.xaml + + + + + + Strings\en-us\Resources.resw + + + + + + Assets\Settings\Icons\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessLaunchContext.cs b/src/settings-ui/QuickAccess.UI/QuickAccessLaunchContext.cs new file mode 100644 index 0000000000..2b01947728 --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/QuickAccessLaunchContext.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.PowerToys.QuickAccess; + +public sealed record QuickAccessLaunchContext(string? ShowEventName, string? ExitEventName, string? RunnerPipeName, string? AppPipeName) +{ + public static QuickAccessLaunchContext Parse(string[] args) + { + string? showEvent = null; + string? exitEvent = null; + string? runnerPipe = null; + string? appPipe = null; + + foreach (var arg in args) + { + if (TryReadValue(arg, "--show-event", out var value)) + { + showEvent = value; + } + else if (TryReadValue(arg, "--exit-event", out value)) + { + exitEvent = value; + } + else if (TryReadValue(arg, "--runner-pipe", out value)) + { + runnerPipe = value; + } + else if (TryReadValue(arg, "--app-pipe", out value)) + { + appPipe = value; + } + } + + return new QuickAccessLaunchContext(showEvent, exitEvent, runnerPipe, appPipe); + } + + private static bool TryReadValue(string candidate, string key, [NotNullWhen(true)] out string? value) + { + if (candidate.StartsWith(key, StringComparison.OrdinalIgnoreCase)) + { + if (candidate.Length == key.Length) + { + value = null; + return false; + } + + if (candidate[key.Length] == '=') + { + value = candidate[(key.Length + 1)..].Trim('"'); + return true; + } + } + + value = null; + return false; + } +} diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml new file mode 100644 index 0000000000..ab7b3c250a --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml.cs b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml.cs new file mode 100644 index 0000000000..b21fb04b24 --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.UI.Xaml; + +namespace Microsoft.PowerToys.QuickAccess; + +public partial class App : Application +{ + private static MainWindow? _window; + + public App() + { + InitializeComponent(); + } + + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + var launchContext = QuickAccessLaunchContext.Parse(Environment.GetCommandLineArgs()); + _window = new MainWindow(launchContext); + _window.Closed += OnWindowClosed; + _window.Activate(); + } + + private static void OnWindowClosed(object sender, WindowEventArgs args) + { + if (sender is MainWindow window) + { + window.Closed -= OnWindowClosed; + } + + _window = null; + } +} diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml new file mode 100644 index 0000000000..c4d010560d --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml.cs b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml.cs new file mode 100644 index 0000000000..1212855fe4 --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/AppsListPage.xaml.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.PowerToys.QuickAccess.ViewModels; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Animation; +using Microsoft.UI.Xaml.Navigation; + +namespace Microsoft.PowerToys.QuickAccess.Flyout; + +public sealed partial class AppsListPage : Page +{ + private FlyoutNavigationContext? _context; + + public AppsListPage() + { + InitializeComponent(); + } + + public AllAppsViewModel ViewModel { get; private set; } = default!; + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + if (e.Parameter is FlyoutNavigationContext context) + { + _context = context; + ViewModel = context.AllAppsViewModel; + DataContext = ViewModel; + ViewModel.RefreshSettings(); + } + } + + private void BackButton_Click(object sender, RoutedEventArgs e) + { + if (_context == null || Frame == null) + { + return; + } + + Frame.Navigate(typeof(LaunchPage), _context, new SlideNavigationTransitionInfo { Effect = SlideNavigationTransitionEffect.FromLeft }); + } + + private void SortAlphabetical_Click(object sender, RoutedEventArgs e) + { + if (ViewModel != null) + { + ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical; + } + } + + private void SortByStatus_Click(object sender, RoutedEventArgs e) + { + if (ViewModel != null) + { + ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus; + } + } +} diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/FlyoutNavigationContext.cs b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/FlyoutNavigationContext.cs new file mode 100644 index 0000000000..3aab3ca334 --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/FlyoutNavigationContext.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.PowerToys.QuickAccess.Services; +using Microsoft.PowerToys.QuickAccess.ViewModels; + +namespace Microsoft.PowerToys.QuickAccess.Flyout; + +internal sealed record FlyoutNavigationContext( + LauncherViewModel LauncherViewModel, + AllAppsViewModel AllAppsViewModel, + IQuickAccessCoordinator Coordinator); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/LaunchPage.xaml similarity index 64% rename from src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml rename to src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/LaunchPage.xaml index 8dea006b08..f2f53b57d4 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Flyout/LaunchPage.xaml @@ -1,5 +1,5 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI.Controls/ModuleList/ModuleList.xaml.cs b/src/settings-ui/Settings.UI.Controls/ModuleList/ModuleList.xaml.cs new file mode 100644 index 0000000000..f9d36a7e69 --- /dev/null +++ b/src/settings-ui/Settings.UI.Controls/ModuleList/ModuleList.xaml.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.PowerToys.Settings.UI.Controls +{ + public sealed partial class ModuleList : UserControl + { + public ModuleList() + { + this.InitializeComponent(); + } + + public Thickness DividerThickness + { + get => (Thickness)GetValue(DividerThicknessProperty); + set => SetValue(DividerThicknessProperty, value); + } + + public static readonly DependencyProperty DividerThicknessProperty = DependencyProperty.Register(nameof(DividerThickness), typeof(Thickness), typeof(ModuleList), new PropertyMetadata(new Thickness(0, 1, 0, 0))); + + public bool IsItemClickable + { + get => (bool)GetValue(IsItemClickableProperty); + set => SetValue(IsItemClickableProperty, value); + } + + public static readonly DependencyProperty IsItemClickableProperty = DependencyProperty.Register(nameof(IsItemClickable), typeof(bool), typeof(ModuleList), new PropertyMetadata(true)); + + public object ItemsSource + { + get => (object)GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + + public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(nameof(ItemsSource), typeof(object), typeof(ModuleList), new PropertyMetadata(null)); + + public ModuleListSortOption SortOption + { + get => (ModuleListSortOption)GetValue(SortOptionProperty); + set => SetValue(SortOptionProperty, value); + } + + public static readonly DependencyProperty SortOptionProperty = DependencyProperty.Register(nameof(SortOption), typeof(ModuleListSortOption), typeof(ModuleList), new PropertyMetadata(ModuleListSortOption.Alphabetical)); + + private void OnSettingsCardClick(object sender, RoutedEventArgs e) + { + if (sender is FrameworkElement element && element.Tag is ModuleListItem item) + { + item.ClickCommand?.Execute(item.Tag); + } + } + } +} diff --git a/src/settings-ui/Settings.UI.Controls/ModuleList/ModuleListItem.cs b/src/settings-ui/Settings.UI.Controls/ModuleList/ModuleListItem.cs new file mode 100644 index 0000000000..ef92e3e592 --- /dev/null +++ b/src/settings-ui/Settings.UI.Controls/ModuleList/ModuleListItem.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows.Input; + +namespace Microsoft.PowerToys.Settings.UI.Controls +{ + public class ModuleListItem : INotifyPropertyChanged + { + private bool _isEnabled; + private string _label = string.Empty; + private string _icon = string.Empty; + private bool _isNew; + private bool _isLocked; + private object? _tag; + private ICommand? _clickCommand; + + public virtual string Label + { + get => _label; + set + { + if (_label != value) + { + _label = value; + OnPropertyChanged(); + } + } + } + + public virtual string Icon + { + get => _icon; + set + { + if (_icon != value) + { + _icon = value; + OnPropertyChanged(); + } + } + } + + public virtual bool IsNew + { + get => _isNew; + set + { + if (_isNew != value) + { + _isNew = value; + OnPropertyChanged(); + } + } + } + + public virtual bool IsLocked + { + get => _isLocked; + set + { + if (_isLocked != value) + { + _isLocked = value; + OnPropertyChanged(); + } + } + } + + public virtual bool IsEnabled + { + get => _isEnabled; + set + { + if (_isEnabled != value) + { + _isEnabled = value; + OnPropertyChanged(); + } + } + } + + public virtual object? Tag + { + get => _tag; + set + { + if (_tag != value) + { + _tag = value; + OnPropertyChanged(); + } + } + } + + public virtual ICommand? ClickCommand + { + get => _clickCommand; + set + { + if (_clickCommand != value) + { + _clickCommand = value; + OnPropertyChanged(); + } + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/settings-ui/Settings.UI.Controls/ModuleList/ModuleListSortOption.cs b/src/settings-ui/Settings.UI.Controls/ModuleList/ModuleListSortOption.cs new file mode 100644 index 0000000000..233c891d6b --- /dev/null +++ b/src/settings-ui/Settings.UI.Controls/ModuleList/ModuleListSortOption.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.PowerToys.Settings.UI.Controls +{ + public enum ModuleListSortOption + { + Alphabetical, + ByStatus, + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml b/src/settings-ui/Settings.UI.Controls/Primitives/Card.xaml similarity index 97% rename from src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml rename to src/settings-ui/Settings.UI.Controls/Primitives/Card.xaml index 9563bfceb3..508d94b7ca 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml +++ b/src/settings-ui/Settings.UI.Controls/Primitives/Card.xaml @@ -21,11 +21,11 @@ BorderThickness="{x:Bind BorderThickness, Mode=OneWay}" CornerRadius="{x:Bind CornerRadius, Mode=OneWay}"> - + - + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml.cs b/src/settings-ui/Settings.UI.Controls/Primitives/Card.xaml.cs similarity index 94% rename from src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml.cs rename to src/settings-ui/Settings.UI.Controls/Primitives/Card.xaml.cs index 1959a70445..ebf04c4e89 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml.cs +++ b/src/settings-ui/Settings.UI.Controls/Primitives/Card.xaml.cs @@ -11,9 +11,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { public static readonly DependencyProperty TitleContentProperty = DependencyProperty.Register(nameof(TitleContent), typeof(object), typeof(Card), new PropertyMetadata(defaultValue: null, OnVisualPropertyChanged)); - public object TitleContent + public object? TitleContent { - get => (object)GetValue(TitleContentProperty); + get => (object?)GetValue(TitleContentProperty); set => SetValue(TitleContentProperty, value); } @@ -34,7 +34,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set => SetValue(ContentProperty, value); } - public static readonly DependencyProperty DividerVisibilityProperty = DependencyProperty.Register(nameof(DividerVisibility), typeof(Visibility), typeof(Card), new PropertyMetadata(defaultValue: null)); + public static readonly DependencyProperty DividerVisibilityProperty = DependencyProperty.Register(nameof(DividerVisibility), typeof(Visibility), typeof(Card), new PropertyMetadata(defaultValue: Visibility.Visible)); public Visibility DividerVisibility { @@ -66,7 +66,6 @@ namespace Microsoft.PowerToys.Settings.UI.Controls else { VisualStateManager.GoToState(this, "TitleGridVisible", true); - DividerVisibility = Visibility.Visible; } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/FlyoutMenuButton.xaml.cs b/src/settings-ui/Settings.UI.Controls/Primitives/FlyoutMenuButton.cs similarity index 96% rename from src/settings-ui/Settings.UI/SettingsXAML/Controls/FlyoutMenuButton.xaml.cs rename to src/settings-ui/Settings.UI.Controls/Primitives/FlyoutMenuButton.cs index bb1767e01e..f313506f23 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/FlyoutMenuButton.xaml.cs +++ b/src/settings-ui/Settings.UI.Controls/Primitives/FlyoutMenuButton.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. diff --git a/src/settings-ui/Settings.UI.Controls/QuickAccess/IQuickAccessLauncher.cs b/src/settings-ui/Settings.UI.Controls/QuickAccess/IQuickAccessLauncher.cs new file mode 100644 index 0000000000..8c35889c94 --- /dev/null +++ b/src/settings-ui/Settings.UI.Controls/QuickAccess/IQuickAccessLauncher.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using ManagedCommon; + +namespace Microsoft.PowerToys.Settings.UI.Controls +{ + public interface IQuickAccessLauncher + { + bool Launch(ModuleType moduleType); + } +} diff --git a/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessItem.cs b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessItem.cs new file mode 100644 index 0000000000..b639d87802 --- /dev/null +++ b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessItem.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Windows.Input; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.UI.Xaml; + +namespace Microsoft.PowerToys.Settings.UI.Controls +{ + public sealed class QuickAccessItem : Observable + { + private string _title = string.Empty; + + public string Title + { + get => _title; + set => Set(ref _title, value); + } + + private string _description = string.Empty; + + public string Description + { + get => _description; + set => Set(ref _description, value); + } + + private string _icon = string.Empty; + + public string Icon + { + get => _icon; + set => Set(ref _icon, value); + } + + private ICommand? _command; + + public ICommand? Command + { + get => _command; + set => Set(ref _command, value); + } + + private object? _commandParameter; + + public object? CommandParameter + { + get => _commandParameter; + set => Set(ref _commandParameter, value); + } + + private bool _visible = true; + + public bool Visible + { + get => _visible; + set => Set(ref _visible, value); + } + + private object? _tag; + + public object? Tag + { + get => _tag; + set => Set(ref _tag, value); + } + } +} diff --git a/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessLauncher.cs b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessLauncher.cs new file mode 100644 index 0000000000..4799ff624f --- /dev/null +++ b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessLauncher.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using PowerToys.Interop; + +namespace Microsoft.PowerToys.Settings.UI.Controls +{ + public class QuickAccessLauncher : IQuickAccessLauncher + { + private readonly bool _isElevated; + + public QuickAccessLauncher(bool isElevated) + { + _isElevated = isElevated; + } + + public virtual bool Launch(ModuleType moduleType) + { + switch (moduleType) + { + case ModuleType.ColorPicker: + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowColorPickerSharedEvent())) + { + eventHandle.Set(); + } + + return true; + case ModuleType.EnvironmentVariables: + { + bool launchAdmin = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.LaunchAdministrator; + string eventName = !_isElevated && launchAdmin + ? Constants.ShowEnvironmentVariablesAdminSharedEvent() + : Constants.ShowEnvironmentVariablesSharedEvent(); + + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName)) + { + eventHandle.Set(); + } + } + + return true; + case ModuleType.FancyZones: + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.FZEToggleEvent())) + { + eventHandle.Set(); + } + + return true; + case ModuleType.Hosts: + { + bool launchAdmin = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.LaunchAdministrator; + string eventName = !_isElevated && launchAdmin + ? Constants.ShowHostsAdminSharedEvent() + : Constants.ShowHostsSharedEvent(); + + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName)) + { + eventHandle.Set(); + } + } + + return true; + case ModuleType.PowerLauncher: + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.PowerLauncherSharedEvent())) + { + eventHandle.Set(); + } + + return true; + case ModuleType.PowerOCR: + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowPowerOCRSharedEvent())) + { + eventHandle.Set(); + } + + return true; + case ModuleType.RegistryPreview: + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.RegistryPreviewTriggerEvent())) + { + eventHandle.Set(); + } + + return true; + case ModuleType.MeasureTool: + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MeasureToolTriggerEvent())) + { + eventHandle.Set(); + } + + return true; + case ModuleType.ShortcutGuide: + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShortcutGuideTriggerEvent())) + { + eventHandle.Set(); + } + + return true; + case ModuleType.CmdPal: + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowCmdPalEvent())) + { + eventHandle.Set(); + } + + return true; + case ModuleType.Workspaces: + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.WorkspacesLaunchEditorEvent())) + { + eventHandle.Set(); + } + + return true; + default: + return false; + } + } + } +} diff --git a/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessList.xaml b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessList.xaml new file mode 100644 index 0000000000..415ae22684 --- /dev/null +++ b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessList.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessList.xaml.cs b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessList.xaml.cs new file mode 100644 index 0000000000..34c4bad013 --- /dev/null +++ b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessList.xaml.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.PowerToys.Settings.UI.Controls +{ + public sealed partial class QuickAccessList : UserControl + { + public QuickAccessList() + { + this.InitializeComponent(); + } + + public object ItemsSource + { + get => (object)GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + + public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(nameof(ItemsSource), typeof(object), typeof(QuickAccessList), new PropertyMetadata(null)); + } +} diff --git a/src/settings-ui/Settings.UI/ViewModels/Flyout/LauncherViewModel.cs b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessViewModel.cs similarity index 50% rename from src/settings-ui/Settings.UI/ViewModels/Flyout/LauncherViewModel.cs rename to src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessViewModel.cs index 7adc4bb933..5531df1ee5 100644 --- a/src/settings-ui/Settings.UI/ViewModels/Flyout/LauncherViewModel.cs +++ b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessViewModel.cs @@ -1,44 +1,64 @@ -// Copyright (c) Microsoft Corporation +// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.ObjectModel; - -using global::PowerToys.GPOWrapper; using ManagedCommon; -using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; +using Microsoft.UI.Dispatching; using Microsoft.Windows.ApplicationModel.Resources; -namespace Microsoft.PowerToys.Settings.UI.ViewModels +namespace Microsoft.PowerToys.Settings.UI.Controls { - public partial class LauncherViewModel : Observable + public partial class QuickAccessViewModel : Observable { - public bool IsUpdateAvailable { get; set; } + private readonly ISettingsRepository _settingsRepository; + private readonly IQuickAccessLauncher _launcher; + private readonly Func _isModuleGpoDisabled; + private readonly ResourceLoader _resourceLoader; + private readonly DispatcherQueue _dispatcherQueue; + private GeneralSettings _generalSettings; - public ObservableCollection FlyoutMenuItems { get; set; } + public ObservableCollection Items { get; } = new(); - private GeneralSettings generalSettingsConfig; - private UpdatingSettings updatingSettingsConfig; - private ISettingsRepository _settingsRepository; - private ResourceLoader resourceLoader; - - private Func SendIPCMessage { get; } - - public LauncherViewModel(ISettingsRepository settingsRepository, Func ipcMSGCallBackFunc) + public QuickAccessViewModel( + ISettingsRepository settingsRepository, + IQuickAccessLauncher launcher, + Func isModuleGpoDisabled, + ResourceLoader resourceLoader) { _settingsRepository = settingsRepository; - generalSettingsConfig = settingsRepository.SettingsConfig; - generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChanged); + _launcher = launcher; + _isModuleGpoDisabled = isModuleGpoDisabled; + _resourceLoader = resourceLoader; + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - // set the callback functions value to handle outgoing IPC message. - SendIPCMessage = ipcMSGCallBackFunc; - resourceLoader = ResourceLoaderInstance.ResourceLoader; - FlyoutMenuItems = new ObservableCollection(); + _generalSettings = _settingsRepository.SettingsConfig; + _generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChanged); + _settingsRepository.SettingsChanged += OnSettingsChanged; + InitializeItems(); + } + + private void OnSettingsChanged(GeneralSettings newSettings) + { + if (_dispatcherQueue != null) + { + _dispatcherQueue.TryEnqueue(() => + { + _generalSettings = newSettings; + _generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChanged); + RefreshItemsVisibility(); + }); + } + } + + private void InitializeItems() + { AddFlyoutMenuItem(ModuleType.ColorPicker); AddFlyoutMenuItem(ModuleType.CmdPal); AddFlyoutMenuItem(ModuleType.EnvironmentVariables); @@ -50,40 +70,50 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels AddFlyoutMenuItem(ModuleType.MeasureTool); AddFlyoutMenuItem(ModuleType.ShortcutGuide); AddFlyoutMenuItem(ModuleType.Workspaces); - - updatingSettingsConfig = UpdatingSettings.LoadSettings(); - if (updatingSettingsConfig == null) - { - updatingSettingsConfig = new UpdatingSettings(); - } - - if (updatingSettingsConfig.State == UpdatingSettings.UpdatingState.ReadyToInstall || updatingSettingsConfig.State == UpdatingSettings.UpdatingState.ReadyToDownload) - { - IsUpdateAvailable = true; - } - else - { - IsUpdateAvailable = false; - } } private void AddFlyoutMenuItem(ModuleType moduleType) { - if (ModuleHelper.GetModuleGpoConfiguration(moduleType) == GpoRuleConfigured.Disabled) + if (_isModuleGpoDisabled(moduleType)) { return; } - FlyoutMenuItems.Add(new FlyoutMenuItem() + Items.Add(new QuickAccessItem { - Label = resourceLoader.GetString(ModuleHelper.GetModuleLabelResourceName(moduleType)), + Title = _resourceLoader.GetString(Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleLabelResourceName(moduleType)), Tag = moduleType, - Visible = ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, moduleType), - ToolTip = GetModuleToolTip(moduleType), - Icon = ModuleHelper.GetModuleTypeFluentIconName(moduleType), + Visible = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType), + Description = GetModuleToolTip(moduleType), + Icon = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleTypeFluentIconName(moduleType), + Command = new RelayCommand(() => _launcher.Launch(moduleType)), }); } + private void ModuleEnabledChanged() + { + if (_dispatcherQueue != null) + { + _dispatcherQueue.TryEnqueue(() => + { + _generalSettings = _settingsRepository.SettingsConfig; + _generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChanged); + RefreshItemsVisibility(); + }); + } + } + + private void RefreshItemsVisibility() + { + foreach (var item in Items) + { + if (item.Tag is ModuleType moduleType) + { + item.Visible = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType); + } + } + } + private string GetModuleToolTip(ModuleType moduleType) { return moduleType switch @@ -99,16 +129,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels }; } - private void ModuleEnabledChanged() - { - generalSettingsConfig = _settingsRepository.SettingsConfig; - generalSettingsConfig.AddEnabledModuleChangeNotification(ModuleEnabledChanged); - foreach (FlyoutMenuItem item in FlyoutMenuItems) - { - item.Visible = ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, item.Tag); - } - } - private string GetShortcutGuideToolTip() { var shortcutGuideSettings = SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig; @@ -116,15 +136,5 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels ? "Win" : shortcutGuideSettings.Properties.OpenShortcutGuide.ToString(); } - - internal void StartBugReport() - { - SendIPCMessage("{\"bugreport\": 0 }"); - } - - internal void KillRunner() - { - SendIPCMessage("{\"killrunner\": 0 }"); - } } } diff --git a/src/settings-ui/Settings.UI.Controls/Settings.UI.Controls.csproj b/src/settings-ui/Settings.UI.Controls/Settings.UI.Controls.csproj new file mode 100644 index 0000000000..fac43d56a4 --- /dev/null +++ b/src/settings-ui/Settings.UI.Controls/Settings.UI.Controls.csproj @@ -0,0 +1,32 @@ + + + + + + Library + net9.0-windows10.0.26100.0 + Microsoft.PowerToys.Settings.UI.Controls + PowerToys.Settings.UI.Controls + true + true + true + PowerToys.Settings.UI.Controls.pri + enable + x64;ARM64 + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI.Controls/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI.Controls/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..6664b14936 --- /dev/null +++ b/src/settings-ui/Settings.UI.Controls/Strings/en-us/Resources.resw @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Sort utilities + + + Alphabetically + + + By status + + + Sort utilities + + + NEW + + + This setting is managed by your organization + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/FlyoutMenuButton.xaml b/src/settings-ui/Settings.UI.Controls/Themes/Generic.xaml similarity index 95% rename from src/settings-ui/Settings.UI/SettingsXAML/Controls/FlyoutMenuButton.xaml rename to src/settings-ui/Settings.UI.Controls/Themes/Generic.xaml index 50c0e4007c..466ad4475b 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/FlyoutMenuButton.xaml +++ b/src/settings-ui/Settings.UI.Controls/Themes/Generic.xaml @@ -1,14 +1,9 @@ - - - -