diff --git a/PowerToys.sln b/PowerToys.sln index f4f2e1bd0f..3bcdafeaca 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -394,6 +394,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.WindowWalk EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerToys.Settings", "src\settings-ui\Settings.UI\PowerToys.Settings.csproj", "{020A7474-3601-4160-A159-D7B70B77B15F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerToys.QuickAccess", "src\settings-ui\QuickAccess.UI\PowerToys.QuickAccess.csproj", "{A7C9C9C6-BA80-4CF3-9DB0-6DBBB4148F60}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameUI", "src\modules\powerrename\PowerRenameUILib\PowerRenameUI.vcxproj", "{27718999-C175-450A-861C-89F911E16A88}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameContextMenu", "src\modules\powerrename\PowerRenameContextMenu\PowerRenameContextMenu.vcxproj", "{1DBBB112-4BB1-444B-8EBB-E66555C76BA6}" @@ -1686,6 +1688,14 @@ Global {020A7474-3601-4160-A159-D7B70B77B15F}.Release|ARM64.Build.0 = Release|ARM64 {020A7474-3601-4160-A159-D7B70B77B15F}.Release|x64.ActiveCfg = Release|x64 {020A7474-3601-4160-A159-D7B70B77B15F}.Release|x64.Build.0 = Release|x64 + {A7C9C9C6-BA80-4CF3-9DB0-6DBBB4148F60}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A7C9C9C6-BA80-4CF3-9DB0-6DBBB4148F60}.Debug|ARM64.Build.0 = Debug|ARM64 + {A7C9C9C6-BA80-4CF3-9DB0-6DBBB4148F60}.Debug|x64.ActiveCfg = Debug|x64 + {A7C9C9C6-BA80-4CF3-9DB0-6DBBB4148F60}.Debug|x64.Build.0 = Debug|x64 + {A7C9C9C6-BA80-4CF3-9DB0-6DBBB4148F60}.Release|ARM64.ActiveCfg = Release|ARM64 + {A7C9C9C6-BA80-4CF3-9DB0-6DBBB4148F60}.Release|ARM64.Build.0 = Release|ARM64 + {A7C9C9C6-BA80-4CF3-9DB0-6DBBB4148F60}.Release|x64.ActiveCfg = Release|x64 + {A7C9C9C6-BA80-4CF3-9DB0-6DBBB4148F60}.Release|x64.Build.0 = Release|x64 {27718999-C175-450A-861C-89F911E16A88}.Debug|ARM64.ActiveCfg = Debug|ARM64 {27718999-C175-450A-861C-89F911E16A88}.Debug|ARM64.Build.0 = Debug|ARM64 {27718999-C175-450A-861C-89F911E16A88}.Debug|x64.ActiveCfg = Debug|x64 @@ -3140,6 +3150,7 @@ Global {FD464B4C-2F68-4D06-91E7-4208146C41F5} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} {8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} {020A7474-3601-4160-A159-D7B70B77B15F} = {C3081D9A-1586-441A-B5F4-ED815B3719C1} + {A7C9C9C6-BA80-4CF3-9DB0-6DBBB4148F60} = {C3081D9A-1586-441A-B5F4-ED815B3719C1} {27718999-C175-450A-861C-89F911E16A88} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} {1DBBB112-4BB1-444B-8EBB-E66555C76BA6} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} {5A1DB2F0-0715-4B3B-98E6-79BC41540045} = {4AFC9975-2456-4C70-94A4-84073C1CED93} diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 4b29149f78..d110c09bc4 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -36,6 +36,7 @@ #include #include "centralized_kb_hook.h" #include "centralized_hotkeys.h" +#include "quick_access_host.h" #if _DEBUG && _WIN64 #include "unhandled_exception_handler.h" @@ -105,6 +106,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow #endif Trace::RegisterProvider(); start_tray_icon(isProcessElevated); + QuickAccessHost::start(); set_tray_icon_visible(get_general_settings().showSystemTrayIcon); CentralizedKeyboardHook::Start(); @@ -242,6 +244,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..b596510349 --- /dev/null +++ b/src/runner/quick_access_host.cpp @@ -0,0 +1,157 @@ +#include "pch.h" +#include "quick_access_host.h" + +#include +#include +#include + +#include +#include +#include + +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::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() + { + quick_access_process.reset(); + show_event.reset(); + exit_event.reset(); + show_event_name.clear(); + exit_event_name.clear(); + } + + std::wstring build_event_name(const wchar_t* suffix) + { + return L"Local\\PowerToysQuickAccess_" + std::to_wstring(GetCurrentProcessId()) + suffix; + } + + 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"\""; + return command_line; + } +} + +namespace QuickAccessHost +{ + bool is_running() + { + std::scoped_lock lock(quick_access_mutex); + return is_process_active_locked(); + } + + void start() + { + std::scoped_lock lock(quick_access_mutex); + if (is_process_active_locked()) + { + 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; + } + + 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() + { + std::unique_lock lock(quick_access_mutex); + if (exit_event) + { + SetEvent(exit_event.get()); + } + + if (quick_access_process) + { + WaitForSingleObject(quick_access_process.get(), 2000); + } + + 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 afff599d8e..a9178f13be 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -66,6 +66,7 @@ + @@ -80,6 +81,7 @@ + diff --git a/src/runner/runner.vcxproj.filters b/src/runner/runner.vcxproj.filters index 812d7857a2..26af9272e2 100644 --- a/src/runner/runner.vcxproj.filters +++ b/src/runner/runner.vcxproj.filters @@ -48,6 +48,9 @@ Utils + + Utils + @@ -99,6 +102,9 @@ Utils + + Utils + diff --git a/src/runner/tray_icon.cpp b/src/runner/tray_icon.cpp index 749c921659..547b4103ad 100644 --- a/src/runner/tray_icon.cpp +++ b/src/runner/tray_icon.cpp @@ -5,6 +5,7 @@ #include "general_settings.h" #include "centralized_hotkeys.h" #include "centralized_kb_hook.h" +#include "quick_access_host.h" #include #include @@ -71,7 +72,8 @@ void change_menu_item_text(const UINT item_id, wchar_t* new_text) void open_quick_access_flyout_window(const POINT flyout_position) { - open_settings_window(std::nullopt, true, flyout_position); + UNREFERENCED_PARAMETER(flyout_position); + QuickAccessHost::show(); } void handle_tray_command(HWND window, const WPARAM command_id, LPARAM lparam) diff --git a/src/settings-ui/QuickAccess.UI/App.xaml b/src/settings-ui/QuickAccess.UI/App.xaml new file mode 100644 index 0000000000..6acda17b6c --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/src/settings-ui/QuickAccess.UI/App.xaml.cs b/src/settings-ui/QuickAccess.UI/App.xaml.cs new file mode 100644 index 0000000000..aaac79fb53 --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/App.xaml.cs @@ -0,0 +1,20 @@ +// 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 MainWindow? _window; + + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + var launchContext = QuickAccessLaunchContext.Parse(Environment.GetCommandLineArgs()); + _window = new MainWindow(launchContext); + _window.Activate(); + } +} diff --git a/src/settings-ui/QuickAccess.UI/MainWindow.xaml b/src/settings-ui/QuickAccess.UI/MainWindow.xaml new file mode 100644 index 0000000000..cba10a7726 --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/MainWindow.xaml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/src/settings-ui/QuickAccess.UI/MainWindow.xaml.cs b/src/settings-ui/QuickAccess.UI/MainWindow.xaml.cs new file mode 100644 index 0000000000..7b889f385c --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/MainWindow.xaml.cs @@ -0,0 +1,418 @@ +// 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.Runtime.InteropServices; +using System.Threading; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Windows.Graphics; +using WinRT.Interop; + +namespace Microsoft.PowerToys.QuickAccess; + +public sealed partial class MainWindow : Window +{ + private readonly QuickAccessLaunchContext _launchContext; + private readonly DispatcherQueue _dispatcherQueue; + private readonly IntPtr _hwnd; + private readonly AppWindow? _appWindow; + private EventWaitHandle? _showEvent; + private EventWaitHandle? _exitEvent; + private ManualResetEventSlim? _listenerShutdownEvent; + private Thread? _showListenerThread; + private Thread? _exitListenerThread; + private bool _isWindowCloaked; + private bool _initialActivationHandled; + private bool _isPrimed; + + private const int DefaultWidth = 320; + private const int DefaultHeight = 480; + private const int DwmWaCloak = 13; + private const int GwlStyle = -16; + private const int SwHide = 0; + private const int SwShow = 5; + private const int SwShowNoActivate = 8; + private const uint SwpShowWindow = 0x0040; + private const uint SwpNoZorder = 0x0004; + private const uint SwpNoSize = 0x0001; + private const uint SwpNoMove = 0x0002; + private const uint SwpNoActivate = 0x0010; + private const uint SwpFrameChanged = 0x0020; + private const long WsSysmenu = 0x00080000L; + private const long WsMinimizeBox = 0x00020000L; + private const long WsMaximizeBox = 0x00010000L; + private static readonly IntPtr HwndTopmost = new(-1); + private static readonly IntPtr HwndBottom = new(1); + + public MainWindow(QuickAccessLaunchContext launchContext) + { + InitializeComponent(); + _launchContext = launchContext; + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + _hwnd = WindowNative.GetWindowHandle(this); + _appWindow = InitializeAppWindow(_hwnd); + Title = "PowerToys Quick Access (Preview)"; + + CustomizeWindowChrome(); + HideWindow(); + InitializeEventListeners(); + Closed += OnClosed; + Activated += OnActivated; + } + + private AppWindow? InitializeAppWindow(IntPtr hwnd) + { + var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hwnd); + return AppWindow.GetFromWindowId(windowId); + } + + private void HideWindow() + { + if (_hwnd != IntPtr.Zero) + { + var cloaked = CloakWindow(); + + if (!ShowWindowNative(_hwnd, SwHide) && _appWindow != null) + { + _appWindow.Hide(); + } + + if (cloaked) + { + ShowWindowNative(_hwnd, SwShowNoActivate); + } + else + { + SetWindowPosNative(_hwnd, HwndBottom, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpNoActivate); + } + } + else if (_appWindow != null) + { + _appWindow.Hide(); + } + } + + private void InitializeEventListeners() + { + if (!string.IsNullOrEmpty(_launchContext.ShowEventName)) + { + try + { + _showEvent = EventWaitHandle.OpenExisting(_launchContext.ShowEventName!); + EnsureListenerInfrastructure(); + StartShowListenerThread(); + } + catch (WaitHandleCannotBeOpenedException) + { + } + } + + if (!string.IsNullOrEmpty(_launchContext.ExitEventName)) + { + try + { + _exitEvent = EventWaitHandle.OpenExisting(_launchContext.ExitEventName!); + EnsureListenerInfrastructure(); + StartExitListenerThread(); + } + catch (WaitHandleCannotBeOpenedException) + { + } + } + } + + private void ShowWindow() + { + if (_hwnd != IntPtr.Zero) + { + UncloakWindow(); + ShowWindowNative(_hwnd, SwShow); + SetWindowPosNative(_hwnd, HwndTopmost, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpShowWindow); + SetForegroundWindowNative(_hwnd); + } + + Activate(); + } + + private void OnActivated(object sender, WindowActivatedEventArgs args) + { + if (args.WindowActivationState == WindowActivationState.Deactivated) + { + HideWindow(); + return; + } + + if (_initialActivationHandled) + { + return; + } + + _initialActivationHandled = true; + PrimeWindow(); + HideWindow(); + } + + private void OnClosed(object sender, WindowEventArgs e) + { + StopEventListeners(); + _showEvent?.Dispose(); + _showEvent = null; + _exitEvent?.Dispose(); + _exitEvent = null; + if (_hwnd != IntPtr.Zero) + { + UncloakWindow(); + } + } + + private void PrimeWindow() + { + if (_isPrimed || _hwnd == IntPtr.Zero) + { + return; + } + + _isPrimed = true; + + if (_appWindow != null) + { + var currentPosition = _appWindow.Position; + _appWindow.MoveAndResize(new RectInt32(currentPosition.X, currentPosition.Y, DefaultWidth, DefaultHeight)); + } + + // Warm up the window while cloaked so the first summon does not pay XAML initialization cost. + var cloaked = CloakWindow(); + if (cloaked) + { + ShowWindowNative(_hwnd, SwShowNoActivate); + } + } + + private bool CloakWindow() + { + if (_hwnd == IntPtr.Zero) + { + return false; + } + + if (_isWindowCloaked) + { + return true; + } + + int cloak = 1; + var result = DwmSetWindowAttribute(_hwnd, DwmWaCloak, ref cloak, sizeof(int)); + if (result == 0) + { + _isWindowCloaked = true; + SetWindowPosNative(_hwnd, HwndBottom, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpNoActivate); + return true; + } + + return false; + } + + private void UncloakWindow() + { + if (_hwnd == IntPtr.Zero || !_isWindowCloaked) + { + return; + } + + int cloak = 0; + var result = DwmSetWindowAttribute(_hwnd, DwmWaCloak, ref cloak, sizeof(int)); + if (result == 0) + { + _isWindowCloaked = false; + } + } + + [DllImport("user32.dll", EntryPoint = "ShowWindow", SetLastError = true)] + private static extern bool ShowWindowNative(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr", SetLastError = true)] + private static extern nint GetWindowLongPtrNative(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", SetLastError = true)] + private static extern nint SetWindowLongPtrNative(IntPtr hWnd, int nIndex, nint dwNewLong); + + [DllImport("user32.dll", EntryPoint = "SetWindowPos", SetLastError = true)] + private static extern bool SetWindowPosNative(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll", EntryPoint = "SetForegroundWindow", SetLastError = true)] + private static extern bool SetForegroundWindowNative(IntPtr hWnd); + + [DllImport("dwmapi.dll", EntryPoint = "DwmSetWindowAttribute", SetLastError = true)] + private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); + + private void EnsureListenerInfrastructure() + { + _listenerShutdownEvent ??= new ManualResetEventSlim(false); + } + + private void StartShowListenerThread() + { + if (_showEvent == null || _listenerShutdownEvent == null || _showListenerThread != null) + { + return; + } + + _showListenerThread = new Thread(ListenForShowEvents) + { + IsBackground = true, + Name = "QuickAccess-ShowEventListener", + }; + _showListenerThread.Start(); + } + + private void StartExitListenerThread() + { + if (_exitEvent == null || _listenerShutdownEvent == null || _exitListenerThread != null) + { + return; + } + + _exitListenerThread = new Thread(ListenForExitEvents) + { + IsBackground = true, + Name = "QuickAccess-ExitEventListener", + }; + _exitListenerThread.Start(); + } + + private void ListenForShowEvents() + { + if (_showEvent == null || _listenerShutdownEvent == null) + { + return; + } + + var handles = new WaitHandle[] { _showEvent, _listenerShutdownEvent.WaitHandle }; + try + { + while (true) + { + var index = WaitHandle.WaitAny(handles); + if (index == 0) + { + _dispatcherQueue.TryEnqueue(ShowWindow); + } + else + { + break; + } + } + } + catch (ObjectDisposedException) + { + } + catch (ThreadInterruptedException) + { + } + } + + private void ListenForExitEvents() + { + if (_exitEvent == null || _listenerShutdownEvent == null) + { + return; + } + + var handles = new WaitHandle[] { _exitEvent, _listenerShutdownEvent.WaitHandle }; + try + { + while (true) + { + var index = WaitHandle.WaitAny(handles); + if (index == 0) + { + _dispatcherQueue.TryEnqueue(Close); + break; + } + + if (index == 1) + { + break; + } + } + } + catch (ObjectDisposedException) + { + } + catch (ThreadInterruptedException) + { + } + } + + private void StopEventListeners() + { + if (_listenerShutdownEvent == null) + { + return; + } + + _listenerShutdownEvent.Set(); + + JoinListenerThread(ref _showListenerThread); + JoinListenerThread(ref _exitListenerThread); + + _listenerShutdownEvent.Dispose(); + _listenerShutdownEvent = null; + } + + private static void JoinListenerThread(ref Thread? thread) + { + if (thread == null) + { + return; + } + + try + { + if (!thread.Join(TimeSpan.FromMilliseconds(250))) + { + thread.Interrupt(); + thread.Join(TimeSpan.FromMilliseconds(250)); + } + } + catch (ThreadInterruptedException) + { + } + catch (ThreadStateException) + { + } + + thread = null; + } + + private void CustomizeWindowChrome() + { + if (_hwnd == IntPtr.Zero) + { + return; + } + + var stylePtr = GetWindowLongPtrNative(_hwnd, GwlStyle); + var lastError = Marshal.GetLastWin32Error(); + if (stylePtr == nint.Zero && lastError != 0) + { + return; + } + + var styleValue = (long)stylePtr; + var newStyleValue = styleValue & ~(WsSysmenu | WsMinimizeBox | WsMaximizeBox); + + if (newStyleValue == styleValue) + { + return; + } + + SetWindowLongPtrNative(_hwnd, GwlStyle, (nint)newStyleValue); + + // Apply the new chrome immediately so caption buttons disappear right away. + SetWindowPosNative(_hwnd, IntPtr.Zero, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpNoZorder | SwpNoActivate | SwpFrameChanged); + } +} 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..da531683f8 --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj @@ -0,0 +1,36 @@ + + + + + + WinExe + net9.0-windows10.0.19041.0 + Microsoft.PowerToys.QuickAccess + PowerToys.QuickAccess + true + None + true + true + false + false + app.manifest + ..\..\..\$(Platform)\$(Configuration)\WinUI3Apps + false + false + enable + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessLaunchContext.cs b/src/settings-ui/QuickAccess.UI/QuickAccessLaunchContext.cs new file mode 100644 index 0000000000..0aa332a566 --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/QuickAccessLaunchContext.cs @@ -0,0 +1,52 @@ +// 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) +{ + public static QuickAccessLaunchContext Parse(string[] args) + { + string? showEvent = null; + string? exitEvent = 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; + } + } + + return new QuickAccessLaunchContext(showEvent, exitEvent); + } + + 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/app.manifest b/src/settings-ui/QuickAccess.UI/app.manifest new file mode 100644 index 0000000000..52dd9cc7fe --- /dev/null +++ b/src/settings-ui/QuickAccess.UI/app.manifest @@ -0,0 +1,16 @@ + + + + + + + true/PM + PerMonitorV2 + + + + + + + +