/ src / settings-ui / QuickAccess.UI / QuickAccessXAML / MainWindow.xaml.cs
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  }