diff --git a/src/runner/quick_access_host.cpp b/src/runner/quick_access_host.cpp index d6ec605f9a..224c8bdf89 100644 --- a/src/runner/quick_access_host.cpp +++ b/src/runner/quick_access_host.cpp @@ -4,11 +4,17 @@ #include #include #include +#include +#include +#include #include #include +#include #include +extern void receive_json_send_to_main_thread(const std::wstring& msg); + namespace { struct PositionPayload @@ -25,7 +31,10 @@ namespace std::wstring show_event_name; std::wstring exit_event_name; std::wstring position_mapping_name; + std::wstring runner_pipe_name; + std::wstring app_pipe_name; PositionPayload* position_payload = nullptr; + std::unique_ptr quick_access_ipc; std::mutex quick_access_mutex; bool is_process_active_locked() @@ -47,6 +56,12 @@ namespace void reset_state_locked() { + if (quick_access_ipc) + { + quick_access_ipc->end(); + quick_access_ipc.reset(); + } + if (position_payload) { UnmapViewOfFile(position_payload); @@ -60,11 +75,19 @@ namespace show_event_name.clear(); exit_event_name.clear(); position_mapping_name.clear(); + runner_pipe_name.clear(); + app_pipe_name.clear(); } std::wstring build_event_name(const wchar_t* suffix) { - return L"Local\\PowerToysQuickAccess_" + std::to_wstring(GetCurrentProcessId()) + 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) @@ -78,6 +101,18 @@ namespace command_line += L"\" --position-map=\""; command_line += position_mapping_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; } } @@ -141,6 +176,61 @@ namespace QuickAccessHost position_payload->y = 0; position_payload->sequence = 0; + 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) { diff --git a/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj b/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj index 7f5c9ad8c1..526039aa4b 100644 --- a/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj +++ b/src/settings-ui/QuickAccess.UI/PowerToys.QuickAccess.csproj @@ -4,7 +4,7 @@ WinExe - net9.0-windows10.0.19041.0 + net9.0-windows10.0.26100.0 Microsoft.PowerToys.QuickAccess PowerToys.QuickAccess true @@ -18,19 +18,79 @@ 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 + + + Controls\FlyoutMenuButton.xaml + + + + + + Controls\FlyoutMenuButton.xaml.cs + + + + + + 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 index f1aecef02b..599c2fc59a 100644 --- a/src/settings-ui/QuickAccess.UI/QuickAccessLaunchContext.cs +++ b/src/settings-ui/QuickAccess.UI/QuickAccessLaunchContext.cs @@ -7,13 +7,15 @@ using System.Diagnostics.CodeAnalysis; namespace Microsoft.PowerToys.QuickAccess; -public sealed record QuickAccessLaunchContext(string? ShowEventName, string? ExitEventName, string? PositionMapName) +public sealed record QuickAccessLaunchContext(string? ShowEventName, string? ExitEventName, string? PositionMapName, string? RunnerPipeName, string? AppPipeName) { public static QuickAccessLaunchContext Parse(string[] args) { string? showEvent = null; string? exitEvent = null; string? positionMap = null; + string? runnerPipe = null; + string? appPipe = null; foreach (var arg in args) { @@ -29,9 +31,17 @@ public sealed record QuickAccessLaunchContext(string? ShowEventName, string? Exi { positionMap = 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, positionMap); + return new QuickAccessLaunchContext(showEvent, exitEvent, positionMap, runnerPipe, appPipe); } private static bool TryReadValue(string candidate, string key, [NotNullWhen(true)] out string? value) diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml index 6acda17b6c..a8fcbcc2a7 100644 --- a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml @@ -3,7 +3,54 @@ x:Class="Microsoft.PowerToys.QuickAccess.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:local="using:Microsoft.PowerToys.QuickAccess"> + xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml.cs b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml.cs index aaac79fb53..b21fb04b24 100644 --- a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml.cs +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/App.xaml.cs @@ -9,12 +9,28 @@ namespace Microsoft.PowerToys.QuickAccess; public partial class App : Application { - private MainWindow? _window; + 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/MainWindow.xaml b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml index 1a93eb2520..f1fc74bfbf 100644 --- a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml @@ -5,7 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:winuiEx="using:WinUIEx" - xmlns:pages="using:Microsoft.PowerToys.QuickAccess.Pages" + xmlns:flyout="using:Microsoft.PowerToys.QuickAccess.Flyout" Width="420" Height="188" MinWidth="420" @@ -17,5 +17,5 @@ mc:Ignorable="d" Title="PowerToys Quick Access (Preview)"> - + diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml.cs b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml.cs index c203f68323..fa9ad30b5b 100644 --- a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml.cs +++ b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/MainWindow.xaml.cs @@ -7,6 +7,9 @@ using System.IO; using System.IO.MemoryMappedFiles; using System.Runtime.InteropServices; using System.Threading; +using Microsoft.PowerToys.QuickAccess.Flyout; +using Microsoft.PowerToys.QuickAccess.Services; +using Microsoft.PowerToys.QuickAccess.ViewModels; using Microsoft.UI.Dispatching; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; @@ -16,12 +19,16 @@ using WinUIEx; namespace Microsoft.PowerToys.QuickAccess; -public sealed partial class MainWindow : WindowEx +public sealed partial class MainWindow : WindowEx, IDisposable { private readonly QuickAccessLaunchContext _launchContext; private readonly DispatcherQueue _dispatcherQueue; private readonly IntPtr _hwnd; private readonly AppWindow? _appWindow; + private readonly LauncherViewModel _launcherViewModel; + private readonly AllAppsViewModel _allAppsViewModel; + private readonly QuickAccessCoordinator _coordinator; + private bool _disposed; private EventWaitHandle? _showEvent; private EventWaitHandle? _exitEvent; private ManualResetEventSlim? _listenerShutdownEvent; @@ -30,6 +37,9 @@ public sealed partial class MainWindow : WindowEx private bool _isWindowCloaked; private bool _initialActivationHandled; private bool _isPrimed; + + // Prevent auto-hide until the window actually gained focus once. + private bool _hasSeenInteractiveActivation; private MemoryMappedFile? _positionMap; private MemoryMappedViewAccessor? _positionView; private PointInt32? _lastRequestedPosition; @@ -65,6 +75,11 @@ public sealed partial class MainWindow : WindowEx _appWindow = InitializeAppWindow(_hwnd); Title = "PowerToys Quick Access (Preview)"; + _coordinator = new QuickAccessCoordinator(this, _launchContext); + _launcherViewModel = new LauncherViewModel(_coordinator); + _allAppsViewModel = new AllAppsViewModel(_coordinator); + ShellHost.Initialize(_coordinator, _launcherViewModel, _allAppsViewModel); + CustomizeWindowChrome(); HideFromTaskbar(); HideWindow(); @@ -105,6 +120,18 @@ public sealed partial class MainWindow : WindowEx } } + internal void RequestHide() + { + if (_dispatcherQueue.HasThreadAccess) + { + HideWindow(); + } + else + { + _dispatcherQueue.TryEnqueue(HideWindow); + } + } + private void InitializeEventListeners() { InitializePositionMapping(); @@ -208,10 +235,17 @@ public sealed partial class MainWindow : WindowEx { if (args.WindowActivationState == WindowActivationState.Deactivated) { + if (!_hasSeenInteractiveActivation) + { + return; + } + HideWindow(); return; } + _hasSeenInteractiveActivation = true; + if (_initialActivationHandled) { return; @@ -224,16 +258,7 @@ public sealed partial class MainWindow : WindowEx private void OnClosed(object sender, WindowEventArgs e) { - StopEventListeners(); - _showEvent?.Dispose(); - _showEvent = null; - _exitEvent?.Dispose(); - _exitEvent = null; - DisposePositionResources(); - if (_hwnd != IntPtr.Zero) - { - UncloakWindow(); - } + Dispose(); } private void PrimeWindow() @@ -308,6 +333,42 @@ public sealed partial class MainWindow : WindowEx } } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + StopEventListeners(); + + _showEvent?.Dispose(); + _showEvent = null; + + _exitEvent?.Dispose(); + _exitEvent = null; + + if (_hwnd != IntPtr.Zero) + { + UncloakWindow(); + } + + DisposePositionResources(); + + _coordinator.Dispose(); + } + + _disposed = true; + } + [DllImport("user32.dll", EntryPoint = "ShowWindow", SetLastError = true)] private static extern bool ShowWindowNative(IntPtr hWnd, int nCmdShow); diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Pages/MainPage.xaml b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Pages/MainPage.xaml deleted file mode 100644 index 7ed242ae68..0000000000 --- a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Pages/MainPage.xaml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - diff --git a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Pages/MainPage.xaml.cs b/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Pages/MainPage.xaml.cs deleted file mode 100644 index e8371b4608..0000000000 --- a/src/settings-ui/QuickAccess.UI/QuickAccessXAML/Pages/MainPage.xaml.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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.Controls; - -namespace Microsoft.PowerToys.QuickAccess.Pages -{ - public sealed partial class MainPage : Page - { - public MainPage() - { - InitializeComponent(); - } - } -}