MainWindow.xaml.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Runtime.InteropServices; 7 using System.Threading; 8 using System.Threading.Tasks; 9 using ManagedCommon; 10 using Microsoft.PowerToys.QuickAccess.Services; 11 using Microsoft.PowerToys.QuickAccess.ViewModels; 12 using Microsoft.UI.Dispatching; 13 using Microsoft.UI.Windowing; 14 using Microsoft.UI.Xaml; 15 using Windows.Graphics; 16 using WinRT.Interop; 17 using WinUIEx; 18 19 namespace Microsoft.PowerToys.QuickAccess; 20 21 public sealed partial class MainWindow : WindowEx, IDisposable 22 { 23 private readonly QuickAccessLaunchContext _launchContext; 24 private readonly DispatcherQueue _dispatcherQueue; 25 private readonly IntPtr _hwnd; 26 private readonly AppWindow? _appWindow; 27 private readonly LauncherViewModel _launcherViewModel; 28 private readonly AllAppsViewModel _allAppsViewModel; 29 private readonly QuickAccessCoordinator _coordinator; 30 private bool _disposed; 31 private EventWaitHandle? _showEvent; 32 private EventWaitHandle? _exitEvent; 33 private ManualResetEventSlim? _listenerShutdownEvent; 34 private Thread? _showListenerThread; 35 private Thread? _exitListenerThread; 36 private bool _isWindowCloaked; 37 private bool _initialActivationHandled; 38 private bool _isPrimed; 39 40 // Prevent auto-hide until the window actually gained focus once. 41 private bool _hasSeenInteractiveActivation; 42 private bool _isVisible; 43 private IntPtr _mouseHook; 44 private LowLevelMouseProc? _mouseHookDelegate; 45 private CancellationTokenSource? _trimCts; 46 47 private const int DefaultWidth = 320; 48 private const int DefaultHeight = 480; 49 private const int DwmWaCloak = 13; 50 private const int GwlStyle = -16; 51 private const int GwlExStyle = -20; 52 private const int SwHide = 0; 53 private const int SwShow = 5; 54 private const int SwShowNoActivate = 8; 55 private const uint SwpShowWindow = 0x0040; 56 private const uint SwpNoZorder = 0x0004; 57 private const uint SwpNoSize = 0x0001; 58 private const uint SwpNoMove = 0x0002; 59 private const uint SwpNoActivate = 0x0010; 60 private const uint SwpFrameChanged = 0x0020; 61 private const long WsSysmenu = 0x00080000L; 62 private const long WsMinimizeBox = 0x00020000L; 63 private const long WsMaximizeBox = 0x00010000L; 64 private const long WsExToolWindow = 0x00000080L; 65 private const uint MonitorDefaulttonearest = 0x00000002; 66 private static readonly IntPtr HwndTopmost = new(-1); 67 private static readonly IntPtr HwndBottom = new(1); 68 69 public MainWindow(QuickAccessLaunchContext launchContext) 70 { 71 InitializeComponent(); 72 _launchContext = launchContext; 73 _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); 74 _hwnd = WindowNative.GetWindowHandle(this); 75 _appWindow = InitializeAppWindow(_hwnd); 76 Title = "PowerToys Quick Access (Preview)"; 77 78 _coordinator = new QuickAccessCoordinator(this, _launchContext); 79 _launcherViewModel = new LauncherViewModel(_coordinator); 80 _allAppsViewModel = new AllAppsViewModel(_coordinator); 81 ShellHost.Initialize(_coordinator, _launcherViewModel, _allAppsViewModel); 82 83 CustomizeWindowChrome(); 84 HideFromTaskbar(); 85 HideWindow(); 86 InitializeEventListeners(); 87 Closed += OnClosed; 88 Activated += OnActivated; 89 } 90 91 private AppWindow? InitializeAppWindow(IntPtr hwnd) 92 { 93 var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hwnd); 94 return AppWindow.GetFromWindowId(windowId); 95 } 96 97 private void HideWindow() 98 { 99 if (_hwnd != IntPtr.Zero) 100 { 101 var cloaked = CloakWindow(); 102 103 if (!ShowWindowNative(_hwnd, SwHide) && _appWindow != null) 104 { 105 _appWindow.Hide(); 106 } 107 108 if (cloaked) 109 { 110 ShowWindowNative(_hwnd, SwShowNoActivate); 111 } 112 else 113 { 114 SetWindowPosNative(_hwnd, HwndBottom, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpNoActivate); 115 } 116 } 117 else if (_appWindow != null) 118 { 119 _appWindow.Hide(); 120 } 121 122 _isVisible = false; 123 RemoveGlobalMouseHook(); 124 125 ScheduleMemoryTrim(); 126 } 127 128 internal void RequestHide() 129 { 130 if (_dispatcherQueue.HasThreadAccess) 131 { 132 HideWindow(); 133 } 134 else 135 { 136 _dispatcherQueue.TryEnqueue(HideWindow); 137 } 138 } 139 140 private void ScheduleMemoryTrim() 141 { 142 CancelMemoryTrim(); 143 _trimCts = new CancellationTokenSource(); 144 var token = _trimCts.Token; 145 146 // Delay the trim to avoid aggressive GC during quick toggles 147 Task.Delay(2000, token).ContinueWith( 148 _ => 149 { 150 if (token.IsCancellationRequested) 151 { 152 return; 153 } 154 155 TrimMemory(); 156 }, 157 token, 158 TaskContinuationOptions.None, 159 TaskScheduler.Default); 160 } 161 162 private void CancelMemoryTrim() 163 { 164 _trimCts?.Cancel(); 165 _trimCts?.Dispose(); 166 _trimCts = null; 167 } 168 169 private void TrimMemory() 170 { 171 GC.Collect(); 172 GC.WaitForPendingFinalizers(); 173 GC.Collect(); 174 SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle, -1, -1); 175 } 176 177 private void InitializeEventListeners() 178 { 179 if (!string.IsNullOrEmpty(_launchContext.ShowEventName)) 180 { 181 try 182 { 183 _showEvent = EventWaitHandle.OpenExisting(_launchContext.ShowEventName!); 184 EnsureListenerInfrastructure(); 185 StartShowListenerThread(); 186 } 187 catch (WaitHandleCannotBeOpenedException) 188 { 189 } 190 } 191 192 if (!string.IsNullOrEmpty(_launchContext.ExitEventName)) 193 { 194 try 195 { 196 _exitEvent = EventWaitHandle.OpenExisting(_launchContext.ExitEventName!); 197 EnsureListenerInfrastructure(); 198 StartExitListenerThread(); 199 } 200 catch (WaitHandleCannotBeOpenedException) 201 { 202 } 203 } 204 } 205 206 private void ShowWindow() 207 { 208 CancelMemoryTrim(); 209 210 if (_hwnd != IntPtr.Zero) 211 { 212 UncloakWindow(); 213 214 ShowWindowNative(_hwnd, SwShow); 215 216 var flags = SwpNoSize | SwpShowWindow; 217 var targetX = 0; 218 var targetY = 0; 219 220 var windowSize = _appWindow?.Size; 221 var windowWidth = windowSize?.Width ?? DefaultWidth; 222 var windowHeight = windowSize?.Height ?? DefaultHeight; 223 224 GetCursorPos(out var cursorPosition); 225 var monitorHandle = MonitorFromPointNative(cursorPosition, MonitorDefaulttonearest); 226 if (monitorHandle != IntPtr.Zero) 227 { 228 var monitorInfo = new MonitorInfo { CbSize = Marshal.SizeOf<MonitorInfo>() }; 229 if (GetMonitorInfoNative(monitorHandle, ref monitorInfo)) 230 { 231 targetX = monitorInfo.RcWork.Right - windowWidth; 232 targetY = monitorInfo.RcWork.Bottom - windowHeight; 233 } 234 } 235 236 SetWindowPosNative(_hwnd, HwndTopmost, targetX, targetY, 0, 0, flags); 237 WindowHelpers.BringToForeground(_hwnd); 238 } 239 240 _hasSeenInteractiveActivation = true; 241 _initialActivationHandled = true; 242 Activate(); 243 _isVisible = true; 244 EnsureGlobalMouseHook(); 245 ShellHost.RefreshIfAppsList(); 246 } 247 248 private void OnActivated(object sender, WindowActivatedEventArgs args) 249 { 250 if (args.WindowActivationState == WindowActivationState.Deactivated) 251 { 252 if (!_hasSeenInteractiveActivation) 253 { 254 return; 255 } 256 257 HideWindow(); 258 return; 259 } 260 261 _hasSeenInteractiveActivation = true; 262 263 if (_initialActivationHandled) 264 { 265 return; 266 } 267 268 _initialActivationHandled = true; 269 PrimeWindow(); 270 HideWindow(); 271 } 272 273 private void OnClosed(object sender, WindowEventArgs e) 274 { 275 Dispose(); 276 } 277 278 private void PrimeWindow() 279 { 280 if (_isPrimed || _hwnd == IntPtr.Zero) 281 { 282 return; 283 } 284 285 _isPrimed = true; 286 287 if (_appWindow != null) 288 { 289 var currentPosition = _appWindow.Position; 290 _appWindow.MoveAndResize(new RectInt32(currentPosition.X, currentPosition.Y, DefaultWidth, DefaultHeight)); 291 } 292 293 // Warm up the window while cloaked so the first summon does not pay XAML initialization cost. 294 var cloaked = CloakWindow(); 295 if (cloaked) 296 { 297 ShowWindowNative(_hwnd, SwShowNoActivate); 298 } 299 } 300 301 private void HideFromTaskbar() 302 { 303 if (_appWindow == null) 304 { 305 return; 306 } 307 308 try 309 { 310 _appWindow.IsShownInSwitchers = false; 311 } 312 catch (NotImplementedException) 313 { 314 // WinUI Will throw if explorer is not running, safely ignore 315 } 316 catch (Exception) 317 { 318 } 319 } 320 321 private bool CloakWindow() 322 { 323 if (_hwnd == IntPtr.Zero) 324 { 325 return false; 326 } 327 328 if (_isWindowCloaked) 329 { 330 return true; 331 } 332 333 int cloak = 1; 334 var result = DwmSetWindowAttribute(_hwnd, DwmWaCloak, ref cloak, sizeof(int)); 335 if (result == 0) 336 { 337 _isWindowCloaked = true; 338 SetWindowPosNative(_hwnd, HwndBottom, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpNoActivate); 339 return true; 340 } 341 342 return false; 343 } 344 345 private void UncloakWindow() 346 { 347 if (_hwnd == IntPtr.Zero || !_isWindowCloaked) 348 { 349 return; 350 } 351 352 int cloak = 0; 353 var result = DwmSetWindowAttribute(_hwnd, DwmWaCloak, ref cloak, sizeof(int)); 354 if (result == 0) 355 { 356 _isWindowCloaked = false; 357 } 358 } 359 360 public void Dispose() 361 { 362 Dispose(true); 363 GC.SuppressFinalize(this); 364 } 365 366 private void Dispose(bool disposing) 367 { 368 if (_disposed) 369 { 370 return; 371 } 372 373 if (disposing) 374 { 375 StopEventListeners(); 376 377 _showEvent?.Dispose(); 378 _showEvent = null; 379 380 _exitEvent?.Dispose(); 381 _exitEvent = null; 382 383 if (_hwnd != IntPtr.Zero && IsWindow(_hwnd)) 384 { 385 UncloakWindow(); 386 } 387 388 RemoveGlobalMouseHook(); 389 390 _coordinator.Dispose(); 391 } 392 393 _disposed = true; 394 } 395 396 [DllImport("user32.dll", SetLastError = true)] 397 [return: MarshalAs(UnmanagedType.Bool)] 398 private static extern bool IsWindow(IntPtr hWnd); 399 400 [DllImport("user32.dll", EntryPoint = "ShowWindow", SetLastError = true)] 401 private static extern bool ShowWindowNative(IntPtr hWnd, int nCmdShow); 402 403 [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr", SetLastError = true)] 404 private static extern nint GetWindowLongPtrNative(IntPtr hWnd, int nIndex); 405 406 [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", SetLastError = true)] 407 private static extern nint SetWindowLongPtrNative(IntPtr hWnd, int nIndex, nint dwNewLong); 408 409 [DllImport("user32.dll", EntryPoint = "SetWindowPos", SetLastError = true)] 410 private static extern bool SetWindowPosNative(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); 411 412 [DllImport("user32.dll", EntryPoint = "SetForegroundWindow", SetLastError = true)] 413 private static extern bool SetForegroundWindowNative(IntPtr hWnd); 414 415 [DllImport("user32.dll", EntryPoint = "GetForegroundWindow", SetLastError = true)] 416 private static extern IntPtr GetForegroundWindowNative(); 417 418 [DllImport("user32.dll", EntryPoint = "GetWindowThreadProcessId", SetLastError = true)] 419 private static extern uint GetWindowThreadProcessIdNative(IntPtr hWnd, IntPtr lpdwProcessId); 420 421 [DllImport("user32.dll", EntryPoint = "AttachThreadInput", SetLastError = true)] 422 private static extern bool AttachThreadInputNative(uint idAttach, uint idAttachTo, bool fAttach); 423 424 [DllImport("dwmapi.dll", EntryPoint = "DwmSetWindowAttribute", SetLastError = true)] 425 private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); 426 427 [DllImport("user32.dll", EntryPoint = "MonitorFromPoint", SetLastError = true)] 428 private static extern IntPtr MonitorFromPointNative(NativePoint pt, uint dwFlags); 429 430 [DllImport("user32.dll", EntryPoint = "GetMonitorInfoW", SetLastError = true)] 431 private static extern bool GetMonitorInfoNative(IntPtr hMonitor, ref MonitorInfo lpmi); 432 433 [DllImport("user32.dll", EntryPoint = "SetWindowsHookExW", SetLastError = true)] 434 private static extern IntPtr SetWindowsHookExNative(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId); 435 436 [DllImport("user32.dll", EntryPoint = "UnhookWindowsHookEx", SetLastError = true)] 437 private static extern bool UnhookWindowsHookExNative(IntPtr hhk); 438 439 [DllImport("user32.dll", EntryPoint = "CallNextHookEx", SetLastError = true)] 440 private static extern IntPtr CallNextHookExNative(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); 441 442 [DllImport("kernel32.dll", EntryPoint = "GetModuleHandleW", SetLastError = true, CharSet = CharSet.Unicode)] 443 private static extern IntPtr GetModuleHandleNative([MarshalAs(UnmanagedType.LPWStr)] string? lpModuleName); 444 445 [DllImport("user32.dll", EntryPoint = "GetWindowRect", SetLastError = true)] 446 private static extern bool GetWindowRectNative(IntPtr hWnd, out Rect rect); 447 448 private void EnsureGlobalMouseHook() 449 { 450 if (_mouseHook != IntPtr.Zero) 451 { 452 return; 453 } 454 455 _mouseHookDelegate ??= LowLevelMouseHookCallback; 456 var moduleHandle = GetModuleHandleNative(null); 457 _mouseHook = SetWindowsHookExNative(WhMouseLl, _mouseHookDelegate, moduleHandle, 0); 458 } 459 460 private void RemoveGlobalMouseHook() 461 { 462 if (_mouseHook == IntPtr.Zero) 463 { 464 return; 465 } 466 467 UnhookWindowsHookExNative(_mouseHook); 468 _mouseHook = IntPtr.Zero; 469 } 470 471 private IntPtr LowLevelMouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam) 472 { 473 if (nCode >= 0 && _isVisible && lParam != IntPtr.Zero && IsMouseButtonDownMessage(wParam)) 474 { 475 var data = Marshal.PtrToStructure<LowLevelMouseInput>(lParam); 476 if (!IsPointInsideWindow(data.Point)) 477 { 478 _dispatcherQueue.TryEnqueue(() => 479 { 480 if (_isVisible) 481 { 482 HideWindow(); 483 } 484 }); 485 } 486 } 487 488 return CallNextHookExNative(_mouseHook, nCode, wParam, lParam); 489 } 490 491 private static bool IsMouseButtonDownMessage(IntPtr wParam) 492 { 493 var message = wParam.ToInt32(); 494 return message == WmLbuttondown || message == WmRbuttondown || message == WmMbuttondown || message == WmXbuttondown; 495 } 496 497 private bool IsPointInsideWindow(NativePoint point) 498 { 499 if (_hwnd == IntPtr.Zero) 500 { 501 return false; 502 } 503 504 if (!GetWindowRectNative(_hwnd, out var rect)) 505 { 506 return false; 507 } 508 509 return point.X >= rect.Left && point.X <= rect.Right && point.Y >= rect.Top && point.Y <= rect.Bottom; 510 } 511 512 private void EnsureListenerInfrastructure() 513 { 514 _listenerShutdownEvent ??= new ManualResetEventSlim(false); 515 } 516 517 private void StartShowListenerThread() 518 { 519 if (_showEvent == null || _listenerShutdownEvent == null || _showListenerThread != null) 520 { 521 return; 522 } 523 524 _showListenerThread = new Thread(ListenForShowEvents) 525 { 526 IsBackground = true, 527 Name = "QuickAccess-ShowEventListener", 528 }; 529 _showListenerThread.Start(); 530 } 531 532 private void StartExitListenerThread() 533 { 534 if (_exitEvent == null || _listenerShutdownEvent == null || _exitListenerThread != null) 535 { 536 return; 537 } 538 539 _exitListenerThread = new Thread(ListenForExitEvents) 540 { 541 IsBackground = true, 542 Name = "QuickAccess-ExitEventListener", 543 }; 544 _exitListenerThread.Start(); 545 } 546 547 private void ListenForShowEvents() 548 { 549 if (_showEvent == null || _listenerShutdownEvent == null) 550 { 551 return; 552 } 553 554 var handles = new WaitHandle[] { _showEvent, _listenerShutdownEvent.WaitHandle }; 555 try 556 { 557 while (true) 558 { 559 var index = WaitHandle.WaitAny(handles); 560 if (index == 0) 561 { 562 _dispatcherQueue.TryEnqueue(ShowWindow); 563 } 564 else 565 { 566 break; 567 } 568 } 569 } 570 catch (ObjectDisposedException) 571 { 572 } 573 catch (ThreadInterruptedException) 574 { 575 } 576 } 577 578 private void ListenForExitEvents() 579 { 580 if (_exitEvent == null || _listenerShutdownEvent == null) 581 { 582 return; 583 } 584 585 var handles = new WaitHandle[] { _exitEvent, _listenerShutdownEvent.WaitHandle }; 586 try 587 { 588 while (true) 589 { 590 var index = WaitHandle.WaitAny(handles); 591 if (index == 0) 592 { 593 _dispatcherQueue.TryEnqueue(Close); 594 break; 595 } 596 597 if (index == 1) 598 { 599 break; 600 } 601 } 602 } 603 catch (ObjectDisposedException) 604 { 605 } 606 catch (ThreadInterruptedException) 607 { 608 } 609 } 610 611 private void StopEventListeners() 612 { 613 if (_listenerShutdownEvent == null) 614 { 615 return; 616 } 617 618 _listenerShutdownEvent.Set(); 619 620 JoinListenerThread(ref _showListenerThread); 621 JoinListenerThread(ref _exitListenerThread); 622 623 _listenerShutdownEvent.Dispose(); 624 _listenerShutdownEvent = null; 625 } 626 627 private static void JoinListenerThread(ref Thread? thread) 628 { 629 if (thread == null) 630 { 631 return; 632 } 633 634 try 635 { 636 if (!thread.Join(TimeSpan.FromMilliseconds(250))) 637 { 638 thread.Interrupt(); 639 thread.Join(TimeSpan.FromMilliseconds(250)); 640 } 641 } 642 catch (ThreadInterruptedException) 643 { 644 } 645 catch (ThreadStateException) 646 { 647 } 648 649 thread = null; 650 } 651 652 private void CustomizeWindowChrome() 653 { 654 if (_hwnd == IntPtr.Zero) 655 { 656 return; 657 } 658 659 var windowAttributesChanged = false; 660 661 var stylePtr = GetWindowLongPtrNative(_hwnd, GwlStyle); 662 var styleError = Marshal.GetLastWin32Error(); 663 if (!(stylePtr == nint.Zero && styleError != 0)) 664 { 665 var styleValue = (long)stylePtr; 666 var newStyleValue = styleValue & ~(WsSysmenu | WsMinimizeBox | WsMaximizeBox); 667 668 if (newStyleValue != styleValue) 669 { 670 SetWindowLongPtrNative(_hwnd, GwlStyle, (nint)newStyleValue); 671 windowAttributesChanged = true; 672 } 673 } 674 675 var exStylePtr = GetWindowLongPtrNative(_hwnd, GwlExStyle); 676 var exStyleError = Marshal.GetLastWin32Error(); 677 if (!(exStylePtr == nint.Zero && exStyleError != 0)) 678 { 679 var exStyleValue = (long)exStylePtr; 680 var newExStyleValue = exStyleValue | WsExToolWindow; 681 if (newExStyleValue != exStyleValue) 682 { 683 SetWindowLongPtrNative(_hwnd, GwlExStyle, (nint)newExStyleValue); 684 windowAttributesChanged = true; 685 } 686 } 687 688 if (windowAttributesChanged) 689 { 690 // Apply the new chrome immediately so caption buttons disappear right away and the tool-window flag takes effect. 691 SetWindowPosNative(_hwnd, IntPtr.Zero, 0, 0, 0, 0, SwpNoMove | SwpNoSize | SwpNoZorder | SwpNoActivate | SwpFrameChanged); 692 } 693 } 694 695 private const int WhMouseLl = 14; 696 private const int WmLbuttondown = 0x0201; 697 private const int WmRbuttondown = 0x0204; 698 private const int WmMbuttondown = 0x0207; 699 700 [DllImport("user32.dll")] 701 private static extern bool GetCursorPos(out NativePoint lpPoint); 702 703 [DllImport("kernel32.dll")] 704 private static extern bool SetProcessWorkingSetSize(IntPtr hProcess, int dwMinimumWorkingSetSize, int dwMaximumWorkingSetSize); 705 706 private const int WmXbuttondown = 0x020B; 707 708 private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam); 709 710 private struct Rect 711 { 712 public int Left; 713 public int Top; 714 public int Right; 715 public int Bottom; 716 } 717 718 [StructLayout(LayoutKind.Sequential)] 719 private struct LowLevelMouseInput 720 { 721 public NativePoint Point; 722 public int MouseData; 723 public int Flags; 724 public int Time; 725 public IntPtr DwExtraInfo; 726 } 727 728 private struct NativePoint 729 { 730 public int X; 731 public int Y; 732 } 733 734 [StructLayout(LayoutKind.Sequential)] 735 private struct MonitorInfo 736 { 737 public int CbSize; 738 public Rect RcMonitor; 739 public Rect RcWork; 740 public uint DwFlags; 741 } 742 }