/ src / modules / cmdpal / Microsoft.CmdPal.UI / 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.Diagnostics;
   6  using System.Runtime.InteropServices;
   7  using CmdPalKeyboardService;
   8  using CommunityToolkit.Mvvm.Messaging;
   9  using ManagedCommon;
  10  using Microsoft.CmdPal.Core.Common.Helpers;
  11  using Microsoft.CmdPal.Core.Common.Services;
  12  using Microsoft.CmdPal.Core.ViewModels.Messages;
  13  using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
  14  using Microsoft.CmdPal.UI.Controls;
  15  using Microsoft.CmdPal.UI.Events;
  16  using Microsoft.CmdPal.UI.Helpers;
  17  using Microsoft.CmdPal.UI.Messages;
  18  using Microsoft.CmdPal.UI.Services;
  19  using Microsoft.CmdPal.UI.ViewModels;
  20  using Microsoft.CmdPal.UI.ViewModels.Messages;
  21  using Microsoft.CmdPal.UI.ViewModels.Services;
  22  using Microsoft.Extensions.DependencyInjection;
  23  using Microsoft.PowerToys.Telemetry;
  24  using Microsoft.UI.Composition;
  25  using Microsoft.UI.Composition.SystemBackdrops;
  26  using Microsoft.UI.Input;
  27  using Microsoft.UI.Windowing;
  28  using Microsoft.UI.Xaml;
  29  using Microsoft.Windows.AppLifecycle;
  30  using Windows.ApplicationModel.Activation;
  31  using Windows.Foundation;
  32  using Windows.Graphics;
  33  using Windows.System;
  34  using Windows.Win32;
  35  using Windows.Win32.Foundation;
  36  using Windows.Win32.Graphics.Dwm;
  37  using Windows.Win32.UI.Input.KeyboardAndMouse;
  38  using Windows.Win32.UI.WindowsAndMessaging;
  39  using WinRT;
  40  using WinUIEx;
  41  using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
  42  
  43  namespace Microsoft.CmdPal.UI;
  44  
  45  public sealed partial class MainWindow : WindowEx,
  46      IRecipient<DismissMessage>,
  47      IRecipient<ShowWindowMessage>,
  48      IRecipient<HideWindowMessage>,
  49      IRecipient<QuitMessage>,
  50      IRecipient<NavigateToPageMessage>,
  51      IRecipient<NavigationDepthMessage>,
  52      IRecipient<SearchQueryMessage>,
  53      IRecipient<ErrorOccurredMessage>,
  54      IRecipient<DragStartedMessage>,
  55      IRecipient<DragCompletedMessage>,
  56      IDisposable
  57  {
  58      [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
  59      [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
  60      private readonly uint WM_TASKBAR_RESTART;
  61      private readonly HWND _hwnd;
  62      private readonly DispatcherTimer _autoGoHomeTimer;
  63      private readonly WNDPROC? _hotkeyWndProc;
  64      private readonly WNDPROC? _originalWndProc;
  65      private readonly List<TopLevelHotkey> _hotkeys = [];
  66      private readonly KeyboardListener _keyboardListener;
  67      private readonly LocalKeyboardListener _localKeyboardListener;
  68      private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new();
  69      private readonly IThemeService _themeService;
  70      private readonly WindowThemeSynchronizer _windowThemeSynchronizer;
  71      private bool _ignoreHotKeyWhenFullScreen = true;
  72      private bool _themeServiceInitialized;
  73  
  74      // Session tracking for telemetry
  75      private Stopwatch? _sessionStopwatch;
  76      private int _sessionCommandsExecuted;
  77      private int _sessionPagesVisited;
  78      private int _sessionSearchQueriesCount;
  79      private int _sessionMaxNavigationDepth;
  80      private int _sessionErrorCount;
  81  
  82      private DesktopAcrylicController? _acrylicController;
  83      private SystemBackdropConfiguration? _configurationSource;
  84      private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
  85  
  86      private WindowPosition _currentWindowPosition = new();
  87  
  88      private bool _preventHideWhenDeactivated;
  89  
  90      private MainWindowViewModel ViewModel { get; }
  91  
  92      public MainWindow()
  93      {
  94          InitializeComponent();
  95  
  96          ViewModel = App.Current.Services.GetService<MainWindowViewModel>()!;
  97  
  98          _autoGoHomeTimer = new DispatcherTimer();
  99          _autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
 100  
 101          _themeService = App.Current.Services.GetRequiredService<IThemeService>();
 102          _themeService.ThemeChanged += ThemeServiceOnThemeChanged;
 103          _windowThemeSynchronizer = new WindowThemeSynchronizer(_themeService, this);
 104  
 105          _hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
 106  
 107          unsafe
 108          {
 109              CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
 110          }
 111  
 112          SetAcrylic();
 113  
 114          _hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
 115  
 116          _keyboardListener = new KeyboardListener();
 117          _keyboardListener.Start();
 118  
 119          _keyboardListener.SetProcessCommand(new CmdPalKeyboardService.ProcessCommand(HandleSummon));
 120  
 121          this.SetIcon();
 122          AppWindow.Title = RS_.GetString("AppName");
 123          RestoreWindowPosition();
 124          UpdateWindowPositionInMemory();
 125  
 126          WeakReferenceMessenger.Default.Register<DismissMessage>(this);
 127          WeakReferenceMessenger.Default.Register<QuitMessage>(this);
 128          WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
 129          WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
 130          WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
 131          WeakReferenceMessenger.Default.Register<NavigationDepthMessage>(this);
 132          WeakReferenceMessenger.Default.Register<SearchQueryMessage>(this);
 133          WeakReferenceMessenger.Default.Register<ErrorOccurredMessage>(this);
 134          WeakReferenceMessenger.Default.Register<DragStartedMessage>(this);
 135          WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
 136  
 137          // Hide our titlebar.
 138          // We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed
 139          // to hide the old caption buttons. Then, in UpdateRegionsForCustomTitleBar,
 140          // we'll make the top drag-able again. (after our content loads)
 141          ExtendsContentIntoTitleBar = true;
 142          AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
 143          SizeChanged += WindowSizeChanged;
 144          RootElement.Loaded += RootElementLoaded;
 145  
 146          WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
 147  
 148          // LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
 149          // member (and instead like, use a local), then the pointer we marshal
 150          // into the WindowLongPtr will be useless after we leave this function,
 151          // and our **WindProc will explode**.
 152          _hotkeyWndProc = HotKeyPrc;
 153          var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
 154          _originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
 155  
 156          // Load our settings, and then also wire up a settings changed handler
 157          HotReloadSettings();
 158          App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
 159  
 160          // Make sure that we update the acrylic theme when the OS theme changes
 161          RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic);
 162  
 163          // Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
 164          NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
 165          {
 166              Summon(string.Empty);
 167          });
 168  
 169          _localKeyboardListener = new LocalKeyboardListener();
 170          _localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
 171          _localKeyboardListener.Start();
 172  
 173          // Force window to be created, and then cloaked. This will offset initial animation when the window is shown.
 174          HideWindow();
 175      }
 176  
 177      private void OnAutoGoHomeTimerOnTick(object? s, object e)
 178      {
 179          _autoGoHomeTimer.Stop();
 180  
 181          // BEAR LOADING: Focus Search must be suppressed here; otherwise it may steal focus (for example, from the system tray icon)
 182          // and prevent the user from opening its context menu.
 183          WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false));
 184      }
 185  
 186      private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
 187      {
 188          UpdateAcrylic();
 189      }
 190  
 191      private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
 192      {
 193          if (e.Key == VirtualKey.GoBack)
 194          {
 195              WeakReferenceMessenger.Default.Send(new GoBackMessage());
 196          }
 197      }
 198  
 199      private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings();
 200  
 201      private void RootElementLoaded(object sender, RoutedEventArgs e)
 202      {
 203          // Now that our content has loaded, we can update our draggable regions
 204          UpdateRegionsForCustomTitleBar();
 205  
 206          // Add dev ribbon if enabled
 207          if (!BuildInfo.IsCiBuild)
 208          {
 209              RootElement.Children.Add(new DevRibbon { Margin = new Thickness(-1, -1, 120, -1) });
 210          }
 211      }
 212  
 213      private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
 214  
 215      private void PositionCentered()
 216      {
 217          var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest);
 218          PositionCentered(displayArea);
 219      }
 220  
 221      private void PositionCentered(DisplayArea displayArea)
 222      {
 223          var position = WindowPositionHelper.CalculateCenteredPosition(
 224              displayArea,
 225              AppWindow.Size,
 226              (int)this.GetDpiForWindow());
 227  
 228          if (position is not null)
 229          {
 230              // Use Move(), not MoveAndResize(). Windows auto-resizes on DPI change via WM_DPICHANGED;
 231              // the helper already accounts for this when calculating the centered position.
 232              AppWindow.Move((PointInt32)position);
 233          }
 234      }
 235  
 236      private void RestoreWindowPosition()
 237      {
 238          var settings = App.Current.Services.GetService<SettingsModel>();
 239          if (settings?.LastWindowPosition is not { Width: > 0, Height: > 0 } savedPosition)
 240          {
 241              PositionCentered();
 242              return;
 243          }
 244  
 245          // MoveAndResize is safe here—we're restoring a saved state at startup,
 246          // not moving a live window between displays.
 247          var newRect = WindowPositionHelper.AdjustRectForVisibility(
 248              savedPosition.ToPhysicalWindowRectangle(),
 249              new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight),
 250              savedPosition.Dpi);
 251  
 252          AppWindow.MoveAndResize(newRect);
 253      }
 254  
 255      private void UpdateWindowPositionInMemory()
 256      {
 257          var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
 258          _currentWindowPosition = new WindowPosition
 259          {
 260              X = AppWindow.Position.X,
 261              Y = AppWindow.Position.Y,
 262              Width = AppWindow.Size.Width,
 263              Height = AppWindow.Size.Height,
 264              Dpi = (int)this.GetDpiForWindow(),
 265              ScreenWidth = displayArea.WorkArea.Width,
 266              ScreenHeight = displayArea.WorkArea.Height,
 267          };
 268      }
 269  
 270      private void HotReloadSettings()
 271      {
 272          var settings = App.Current.Services.GetService<SettingsModel>()!;
 273  
 274          SetupHotkey(settings);
 275          App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
 276  
 277          _ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
 278  
 279          _autoGoHomeInterval = settings.AutoGoHomeInterval;
 280          _autoGoHomeTimer.Interval = _autoGoHomeInterval;
 281      }
 282  
 283      private void SetAcrylic()
 284      {
 285          if (DesktopAcrylicController.IsSupported())
 286          {
 287              // Hooking up the policy object.
 288              _configurationSource = new SystemBackdropConfiguration
 289              {
 290                  // Initial configuration state.
 291                  IsInputActive = true,
 292              };
 293              UpdateAcrylic();
 294          }
 295      }
 296  
 297      private void UpdateAcrylic()
 298      {
 299          try
 300          {
 301              if (_acrylicController != null)
 302              {
 303                  _acrylicController.RemoveAllSystemBackdropTargets();
 304                  _acrylicController.Dispose();
 305              }
 306  
 307              var backdrop = _themeService.Current.BackdropParameters;
 308              _acrylicController = new DesktopAcrylicController
 309              {
 310                  TintColor = backdrop.TintColor,
 311                  TintOpacity = backdrop.TintOpacity,
 312                  FallbackColor = backdrop.FallbackColor,
 313                  LuminosityOpacity = backdrop.LuminosityOpacity,
 314              };
 315  
 316              // Enable the system backdrop.
 317              // Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
 318              _acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
 319              _acrylicController.SetSystemBackdropConfiguration(_configurationSource);
 320          }
 321          catch (Exception ex)
 322          {
 323              Logger.LogError("Failed to update backdrop", ex);
 324          }
 325      }
 326  
 327      private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
 328      {
 329          StopAutoGoHome();
 330  
 331          var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
 332  
 333          // Remember, IsIconic == "minimized", which is entirely different state
 334          // from "show/hide"
 335          // If we're currently minimized, restore us first, before we reveal
 336          // our window. Otherwise, we'd just be showing a minimized window -
 337          // which would remain not visible to the user.
 338          if (PInvoke.IsIconic(hwnd))
 339          {
 340              // Make sure our HWND is cloaked before any possible window manipulations
 341              Cloak();
 342  
 343              PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
 344          }
 345  
 346          if (target == MonitorBehavior.ToLast)
 347          {
 348              var originalScreen = new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight);
 349              var newRect = WindowPositionHelper.AdjustRectForVisibility(_currentWindowPosition.ToPhysicalWindowRectangle(), originalScreen, _currentWindowPosition.Dpi);
 350              AppWindow.MoveAndResize(newRect);
 351          }
 352          else
 353          {
 354              var display = GetScreen(hwnd, target);
 355              PositionCentered(display);
 356          }
 357  
 358          // Check if the debugger is attached. If it is, we don't want to apply the tool window style,
 359          // because that would make it hard to debug the app
 360          if (Debugger.IsAttached)
 361          {
 362              _hiddenOwnerBehavior.ShowInTaskbar(this, true);
 363          }
 364  
 365          // Just to be sure, SHOW our hwnd.
 366          PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_SHOW);
 367  
 368          // Once we're done, uncloak to avoid all animations
 369          Uncloak();
 370  
 371          PInvoke.SetForegroundWindow(hwnd);
 372          PInvoke.SetActiveWindow(hwnd);
 373  
 374          // Push our window to the top of the Z-order and make it the topmost, so that it appears above all other windows.
 375          // We want to remove the topmost status when we hide the window (because we cloak it instead of hiding it).
 376          PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
 377      }
 378  
 379      private static DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
 380      {
 381          // Leaving a note here, in case we ever need it:
 382          // https://github.com/microsoft/microsoft-ui-xaml/issues/6454
 383          // If we need to ever FindAll, we'll need to iterate manually
 384          var displayAreas = Microsoft.UI.Windowing.DisplayArea.FindAll();
 385          switch (target)
 386          {
 387              case MonitorBehavior.InPlace:
 388                  if (PInvoke.GetWindowRect(currentHwnd, out var bounds))
 389                  {
 390                      RectInt32 converted = new(bounds.X, bounds.Y, bounds.Width, bounds.Height);
 391                      return DisplayArea.GetFromRect(converted, DisplayAreaFallback.Nearest);
 392                  }
 393  
 394                  break;
 395  
 396              case MonitorBehavior.ToFocusedWindow:
 397                  var foregroundWindowHandle = PInvoke.GetForegroundWindow();
 398                  if (foregroundWindowHandle != IntPtr.Zero)
 399                  {
 400                      if (PInvoke.GetWindowRect(foregroundWindowHandle, out var fgBounds))
 401                      {
 402                          RectInt32 converted = new(fgBounds.X, fgBounds.Y, fgBounds.Width, fgBounds.Height);
 403                          return DisplayArea.GetFromRect(converted, DisplayAreaFallback.Nearest);
 404                      }
 405                  }
 406  
 407                  break;
 408  
 409              case MonitorBehavior.ToPrimary:
 410                  return DisplayArea.Primary;
 411  
 412              case MonitorBehavior.ToMouse:
 413              default:
 414                  if (PInvoke.GetCursorPos(out var cursorPos))
 415                  {
 416                      return DisplayArea.GetFromPoint(new PointInt32(cursorPos.X, cursorPos.Y), DisplayAreaFallback.Nearest);
 417                  }
 418  
 419                  break;
 420          }
 421  
 422          return DisplayArea.Primary;
 423      }
 424  
 425      public void Receive(ShowWindowMessage message)
 426      {
 427          var settings = App.Current.Services.GetService<SettingsModel>()!;
 428  
 429          // Start session tracking
 430          _sessionStopwatch = Stopwatch.StartNew();
 431          _sessionCommandsExecuted = 0;
 432          _sessionPagesVisited = 0;
 433  
 434          ShowHwnd(message.Hwnd, settings.SummonOn);
 435      }
 436  
 437      public void Receive(HideWindowMessage message)
 438      {
 439          // This might come in off the UI thread. Make sure to hop back.
 440          DispatcherQueue.TryEnqueue(() =>
 441          {
 442              EndSession("Hide");
 443              HideWindow();
 444          });
 445      }
 446  
 447      public void Receive(QuitMessage message) =>
 448  
 449          // This might come in on a background thread
 450          DispatcherQueue.TryEnqueue(() => Close());
 451  
 452      public void Receive(DismissMessage message)
 453      {
 454          if (message.ForceGoHome)
 455          {
 456              WeakReferenceMessenger.Default.Send(new GoHomeMessage(false, false));
 457          }
 458  
 459          // This might come in off the UI thread. Make sure to hop back.
 460          DispatcherQueue.TryEnqueue(() =>
 461          {
 462              EndSession("Dismiss");
 463              HideWindow();
 464          });
 465      }
 466  
 467      // Session telemetry: Track metrics during the Command Palette session
 468      // These receivers increment counters that are sent when EndSession is called
 469      public void Receive(NavigateToPageMessage message)
 470      {
 471          _sessionPagesVisited++;
 472      }
 473  
 474      public void Receive(NavigationDepthMessage message)
 475      {
 476          if (message.Depth > _sessionMaxNavigationDepth)
 477          {
 478              _sessionMaxNavigationDepth = message.Depth;
 479          }
 480      }
 481  
 482      public void Receive(SearchQueryMessage message)
 483      {
 484          _sessionSearchQueriesCount++;
 485      }
 486  
 487      public void Receive(ErrorOccurredMessage message)
 488      {
 489          _sessionErrorCount++;
 490      }
 491  
 492      /// <summary>
 493      /// Ends the current telemetry session and emits the CmdPal_SessionDuration event.
 494      /// Aggregates all session metrics collected since ShowWindow and sends them to telemetry.
 495      /// </summary>
 496      /// <param name="dismissalReason">The reason the session ended (e.g., Dismiss, Hide, LostFocus).</param>
 497      private void EndSession(string dismissalReason)
 498      {
 499          if (_sessionStopwatch is not null)
 500          {
 501              _sessionStopwatch.Stop();
 502              TelemetryForwarder.LogSessionDuration(
 503                  (ulong)_sessionStopwatch.ElapsedMilliseconds,
 504                  _sessionCommandsExecuted,
 505                  _sessionPagesVisited,
 506                  dismissalReason,
 507                  _sessionSearchQueriesCount,
 508                  _sessionMaxNavigationDepth,
 509                  _sessionErrorCount);
 510              _sessionStopwatch = null;
 511          }
 512      }
 513  
 514      /// <summary>
 515      /// Increments the session commands executed counter for telemetry.
 516      /// Called by TelemetryForwarder when an extension command is invoked.
 517      /// </summary>
 518      internal void IncrementCommandsExecuted()
 519      {
 520          _sessionCommandsExecuted++;
 521      }
 522  
 523      private void HideWindow()
 524      {
 525          // Cloak our HWND to avoid all animations.
 526          var cloaked = Cloak();
 527  
 528          // Then hide our HWND, to make sure that the OS gives the FG / focus back to another app
 529          // (there's no way for us to guess what the right hwnd might be, only the OS can do it right)
 530          PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
 531  
 532          if (cloaked)
 533          {
 534              // TRICKY: show our HWND again. This will trick XAML into painting our
 535              // HWND again, so that we avoid the "flicker" caused by a WinUI3 app
 536              // window being first shown
 537              // SW_SHOWNA will prevent us for trying to fight the focus back
 538              PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_SHOWNA);
 539  
 540              // Intentionally leave the window cloaked. So our window is "visible",
 541              // but also cloaked, so you can't see it.
 542  
 543              // If the window was not cloaked, then leave it hidden.
 544              // Sure, it's not ideal, but at least it's not visible.
 545          }
 546  
 547          // Start auto-go-home timer
 548          RestartAutoGoHome();
 549      }
 550  
 551      private void StopAutoGoHome()
 552      {
 553          _autoGoHomeTimer.Stop();
 554      }
 555  
 556      private void RestartAutoGoHome()
 557      {
 558          if (_autoGoHomeInterval == Timeout.InfiniteTimeSpan)
 559          {
 560              return;
 561          }
 562  
 563          _autoGoHomeTimer.Stop();
 564          _autoGoHomeTimer.Start();
 565      }
 566  
 567      private bool Cloak()
 568      {
 569          bool wasCloaked;
 570          unsafe
 571          {
 572              BOOL value = true;
 573              var hr = PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
 574              if (hr.Failed)
 575              {
 576                  Logger.LogWarning($"DWM cloaking of the main window failed. HRESULT: {hr.Value}.");
 577              }
 578  
 579              wasCloaked = hr.Succeeded;
 580          }
 581  
 582          if (wasCloaked)
 583          {
 584              // Because we're only cloaking the window, bury it at the bottom in case something can
 585              // see it - e.g. some accessibility helper (note: this also removes the top-most status).
 586              PInvoke.SetWindowPos(_hwnd, HWND.HWND_BOTTOM, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
 587          }
 588  
 589          return wasCloaked;
 590      }
 591  
 592      private void Uncloak()
 593      {
 594          unsafe
 595          {
 596              BOOL value = false;
 597              PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
 598          }
 599      }
 600  
 601      internal void MainWindow_Closed(object sender, WindowEventArgs args)
 602      {
 603          var serviceProvider = App.Current.Services;
 604          UpdateWindowPositionInMemory();
 605  
 606          var settings = serviceProvider.GetService<SettingsModel>();
 607          if (settings is not null)
 608          {
 609              settings.LastWindowPosition = new WindowPosition
 610              {
 611                  X = _currentWindowPosition.X,
 612                  Y = _currentWindowPosition.Y,
 613                  Width = _currentWindowPosition.Width,
 614                  Height = _currentWindowPosition.Height,
 615                  Dpi = _currentWindowPosition.Dpi,
 616                  ScreenWidth = _currentWindowPosition.ScreenWidth,
 617                  ScreenHeight = _currentWindowPosition.ScreenHeight,
 618              };
 619  
 620              SettingsModel.SaveSettings(settings);
 621          }
 622  
 623          var extensionService = serviceProvider.GetService<IExtensionService>()!;
 624          extensionService.SignalStopExtensionsAsync();
 625  
 626          App.Current.Services.GetService<TrayIconService>()!.Destroy();
 627  
 628          // WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592).
 629          // Workaround by turning it off before shutdown.
 630          App.Current.DebugSettings.FailFastOnErrors = false;
 631          _localKeyboardListener.Dispose();
 632          DisposeAcrylic();
 633  
 634          _keyboardListener.Stop();
 635          Environment.Exit(0);
 636      }
 637  
 638      private void DisposeAcrylic()
 639      {
 640          if (_acrylicController is not null)
 641          {
 642              _acrylicController.Dispose();
 643              _acrylicController = null!;
 644              _configurationSource = null!;
 645          }
 646      }
 647  
 648      // Updates our window s.t. the top of the window is draggable.
 649      private void UpdateRegionsForCustomTitleBar()
 650      {
 651          var xamlRoot = RootElement.XamlRoot;
 652          if (xamlRoot is null)
 653          {
 654              return;
 655          }
 656  
 657          // Specify the interactive regions of the title bar.
 658          var scaleAdjustment = xamlRoot.RasterizationScale;
 659  
 660          // Get the rectangle around our XAML content. We're going to mark this
 661          // rectangle as "Passthrough", so that the normal window operations
 662          // (resizing, dragging) don't apply in this space.
 663          var transform = RootElement.TransformToVisual(null);
 664  
 665          // Reserve 16px of space at the top for dragging.
 666          var topHeight = 16;
 667          var bounds = transform.TransformBounds(new Rect(
 668              0,
 669              topHeight,
 670              RootElement.ActualWidth,
 671              RootElement.ActualHeight));
 672          var contentRect = GetRect(bounds, scaleAdjustment);
 673          var rectArray = new RectInt32[] { contentRect };
 674          var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(this.AppWindow.Id);
 675          nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rectArray);
 676  
 677          // Add a drag-able region on top
 678          var w = RootElement.ActualWidth;
 679          _ = RootElement.ActualHeight;
 680          var dragSides = new RectInt32[]
 681          {
 682              GetRect(new Rect(0, 0, w, topHeight), scaleAdjustment), // the top, {topHeight=16} tall
 683          };
 684          nonClientInputSrc.SetRegionRects(NonClientRegionKind.Caption, dragSides);
 685      }
 686  
 687      private static RectInt32 GetRect(Rect bounds, double scale)
 688      {
 689          return new RectInt32(
 690              _X: (int)Math.Round(bounds.X * scale),
 691              _Y: (int)Math.Round(bounds.Y * scale),
 692              _Width: (int)Math.Round(bounds.Width * scale),
 693              _Height: (int)Math.Round(bounds.Height * scale));
 694      }
 695  
 696      internal void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
 697      {
 698          if (!_themeServiceInitialized && args.WindowActivationState != WindowActivationState.Deactivated)
 699          {
 700              try
 701              {
 702                  _themeService.Initialize();
 703                  _themeServiceInitialized = true;
 704              }
 705              catch (Exception ex)
 706              {
 707                  Logger.LogError("Failed to initialize ThemeService", ex);
 708              }
 709          }
 710  
 711          if (args.WindowActivationState == WindowActivationState.Deactivated)
 712          {
 713              // Save the current window position before hiding the window
 714              UpdateWindowPositionInMemory();
 715  
 716              // If there's a debugger attached...
 717              if (System.Diagnostics.Debugger.IsAttached)
 718              {
 719                  // ... then don't hide the window when it loses focus.
 720                  return;
 721              }
 722  
 723              // Are we disabled? If we are, then we don't want to dismiss on focus lost.
 724              // This can happen if an extension wanted to show a modal dialog on top of our
 725              // window i.e. in the case of an MSAL auth window.
 726              if (PInvoke.IsWindowEnabled(_hwnd) == 0)
 727              {
 728                  return;
 729              }
 730  
 731              // We're doing something that requires us to lose focus, but we don't want to hide the window
 732              if (_preventHideWhenDeactivated)
 733              {
 734                  return;
 735              }
 736  
 737              // This will DWM cloak our window:
 738              EndSession("LostFocus");
 739              HideWindow();
 740  
 741              PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
 742          }
 743  
 744          if (_configurationSource is not null)
 745          {
 746              _configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
 747          }
 748      }
 749  
 750      public void HandleLaunchNonUI(AppActivationArguments? activatedEventArgs)
 751      {
 752          // LOAD BEARING
 753          // Any reading and processing of the activation arguments must be done
 754          // synchronously in this method, before it returns. The sending instance
 755          // remains blocked until this returns; afterward it may quit, causing
 756          // the activation arguments to be lost.
 757          if (activatedEventArgs is null)
 758          {
 759              Summon(string.Empty);
 760              return;
 761          }
 762  
 763          try
 764          {
 765              if (activatedEventArgs.Kind == ExtendedActivationKind.StartupTask)
 766              {
 767                  return;
 768              }
 769  
 770              if (activatedEventArgs.Kind == ExtendedActivationKind.Protocol)
 771              {
 772                  if (activatedEventArgs.Data is IProtocolActivatedEventArgs protocolArgs)
 773                  {
 774                      if (protocolArgs.Uri.ToString() is string uri)
 775                      {
 776                          // was the URI "x-cmdpal://background" ?
 777                          if (uri.StartsWith("x-cmdpal://background", StringComparison.OrdinalIgnoreCase))
 778                          {
 779                              // we're running, we don't want to activate our window. bail
 780                              return;
 781                          }
 782                          else if (uri.StartsWith("x-cmdpal://settings", StringComparison.OrdinalIgnoreCase))
 783                          {
 784                              WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
 785                              return;
 786                          }
 787                          else if (uri.StartsWith("x-cmdpal://reload", StringComparison.OrdinalIgnoreCase))
 788                          {
 789                              var settings = App.Current.Services.GetService<SettingsModel>();
 790                              if (settings?.AllowExternalReload == true)
 791                              {
 792                                  Logger.LogInfo("External Reload triggered");
 793                                  WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
 794                              }
 795                              else
 796                              {
 797                                  Logger.LogInfo("External Reload is disabled");
 798                              }
 799  
 800                              return;
 801                          }
 802                      }
 803                  }
 804              }
 805          }
 806          catch (COMException ex)
 807          {
 808              // https://learn.microsoft.com/en-us/windows/win32/rpc/rpc-return-values
 809              const int RPC_S_SERVER_UNAVAILABLE = -2147023174;
 810              const int RPC_S_CALL_FAILED = 2147023170;
 811  
 812              // Accessing properties activatedEventArgs.Kind and activatedEventArgs.Data might cause COMException
 813              // if the args are not valid or not passed correctly.
 814              if (ex.HResult is RPC_S_SERVER_UNAVAILABLE or RPC_S_CALL_FAILED)
 815              {
 816                  Logger.LogWarning(
 817                      $"COM exception (HRESULT {ex.HResult}) when accessing activation arguments. " +
 818                      $"This might be due to the calling application not passing them correctly or exiting before we could read them. " +
 819                      $"The application will continue running and fall back to showing the Command Palette window.");
 820              }
 821              else
 822              {
 823                  Logger.LogError(
 824                      $"COM exception (HRESULT {ex.HResult}) when activating the application. " +
 825                      $"The application will continue running and fall back to showing the Command Palette window.",
 826                      ex);
 827              }
 828          }
 829  
 830          Summon(string.Empty);
 831      }
 832  
 833      public void Summon(string commandId) =>
 834  
 835          // The actual showing and hiding of the window will be done by the
 836          // ShellPage. This is because we don't want to show the window if the
 837          // user bound a hotkey to just an invokable command, which we can't
 838          // know till the message is being handled.
 839          WeakReferenceMessenger.Default.Send<HotkeySummonMessage>(new(commandId, _hwnd));
 840  
 841      private void UnregisterHotkeys()
 842      {
 843          _keyboardListener.ClearHotkeys();
 844  
 845          while (_hotkeys.Count > 0)
 846          {
 847              PInvoke.UnregisterHotKey(_hwnd, _hotkeys.Count - 1);
 848              _hotkeys.RemoveAt(_hotkeys.Count - 1);
 849          }
 850      }
 851  
 852      private void SetupHotkey(SettingsModel settings)
 853      {
 854          UnregisterHotkeys();
 855  
 856          var globalHotkey = settings.Hotkey;
 857          if (globalHotkey is not null)
 858          {
 859              if (settings.UseLowLevelGlobalHotkey)
 860              {
 861                  _keyboardListener.SetHotkeyAction(globalHotkey.Win, globalHotkey.Ctrl, globalHotkey.Shift, globalHotkey.Alt, (byte)globalHotkey.Code, string.Empty);
 862  
 863                  _hotkeys.Add(new(globalHotkey, string.Empty));
 864              }
 865              else
 866              {
 867                  var vk = globalHotkey.Code;
 868                  var modifiers =
 869                                  (globalHotkey.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) |
 870                                  (globalHotkey.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) |
 871                                  (globalHotkey.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) |
 872                                  (globalHotkey.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0)
 873                                  ;
 874  
 875                  var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk);
 876                  if (success)
 877                  {
 878                      _hotkeys.Add(new(globalHotkey, string.Empty));
 879                  }
 880              }
 881          }
 882  
 883          foreach (var commandHotkey in settings.CommandHotkeys)
 884          {
 885              var key = commandHotkey.Hotkey;
 886  
 887              if (key is not null)
 888              {
 889                  if (settings.UseLowLevelGlobalHotkey)
 890                  {
 891                      _keyboardListener.SetHotkeyAction(key.Win, key.Ctrl, key.Shift, key.Alt, (byte)key.Code, commandHotkey.CommandId);
 892  
 893                      _hotkeys.Add(new(globalHotkey, string.Empty));
 894                  }
 895                  else
 896                  {
 897                      var vk = key.Code;
 898                      var modifiers =
 899                          (key.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) |
 900                          (key.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) |
 901                          (key.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) |
 902                          (key.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0)
 903                          ;
 904  
 905                      var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk);
 906                      if (success)
 907                      {
 908                          _hotkeys.Add(commandHotkey);
 909                      }
 910                  }
 911              }
 912          }
 913      }
 914  
 915      private void HandleSummon(string commandId)
 916      {
 917          if (_ignoreHotKeyWhenFullScreen)
 918          {
 919              // If we're in full screen mode, ignore the hotkey
 920              if (WindowHelper.IsWindowFullscreen())
 921              {
 922                  return;
 923              }
 924          }
 925  
 926          HandleSummonCore(commandId);
 927      }
 928  
 929      private void HandleSummonCore(string commandId)
 930      {
 931          var isRootHotkey = string.IsNullOrEmpty(commandId);
 932          PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
 933  
 934          var isVisible = this.Visible;
 935  
 936          unsafe
 937          {
 938              // We need to check if our window is cloaked or not. A cloaked window is still
 939              // technically visible, because SHOW/HIDE != iconic (minimized) != cloaked
 940              // (these are all separate states)
 941              long attr = 0;
 942              PInvoke.DwmGetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAKED, &attr, sizeof(long));
 943              if (attr == 1 /* DWM_CLOAKED_APP */)
 944              {
 945                  isVisible = false;
 946              }
 947          }
 948  
 949          // Note to future us: the wParam will have the index of the hotkey we registered.
 950          // We can use that in the future to differentiate the hotkeys we've pressed
 951          // so that we can bind hotkeys to individual commands
 952          if (!isVisible || !isRootHotkey)
 953          {
 954              Summon(commandId);
 955          }
 956          else if (isRootHotkey)
 957          {
 958              // If there's a debugger attached...
 959              if (System.Diagnostics.Debugger.IsAttached)
 960              {
 961                  // ... then manually hide our window. When debugged, we won't get the cool cloaking,
 962                  // but that's the price to pay for having the HWND not light-dismiss while we're debugging.
 963                  Cloak();
 964                  this.Hide();
 965  
 966                  return;
 967              }
 968  
 969              HideWindow();
 970          }
 971      }
 972  
 973      private LRESULT HotKeyPrc(
 974          HWND hwnd,
 975          uint uMsg,
 976          WPARAM wParam,
 977          LPARAM lParam)
 978      {
 979          switch (uMsg)
 980          {
 981              // Prevent the window from maximizing when double-clicking the title bar area
 982              case PInvoke.WM_NCLBUTTONDBLCLK:
 983                  return (LRESULT)IntPtr.Zero;
 984              case PInvoke.WM_HOTKEY:
 985                  {
 986                      var hotkeyIndex = (int)wParam.Value;
 987                      if (hotkeyIndex < _hotkeys.Count)
 988                      {
 989                          var hotkey = _hotkeys[hotkeyIndex];
 990                          HandleSummon(hotkey.CommandId);
 991                      }
 992  
 993                      return (LRESULT)IntPtr.Zero;
 994                  }
 995  
 996              default:
 997                  if (uMsg == WM_TASKBAR_RESTART)
 998                  {
 999                      HotReloadSettings();
1000                  }
1001  
1002                  break;
1003          }
1004  
1005          return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
1006      }
1007  
1008      public void Dispose()
1009      {
1010          _localKeyboardListener.Dispose();
1011          _windowThemeSynchronizer.Dispose();
1012          DisposeAcrylic();
1013      }
1014  
1015      public void Receive(DragStartedMessage message)
1016      {
1017          _preventHideWhenDeactivated = true;
1018      }
1019  
1020      public void Receive(DragCompletedMessage message)
1021      {
1022          _preventHideWhenDeactivated = false;
1023          Task.Delay(200).ContinueWith(_ =>
1024          {
1025              DispatcherQueue.TryEnqueue(StealForeground);
1026          });
1027      }
1028  
1029      private unsafe void StealForeground()
1030      {
1031          var foregroundWindow = PInvoke.GetForegroundWindow();
1032          if (foregroundWindow == _hwnd)
1033          {
1034              return;
1035          }
1036  
1037          // This is bad, evil, and I'll have to forgo today's dinner dessert to punish myself
1038          // for  writing this. But there's no way to make this work without it.
1039          // If the window is not reactivated, the UX breaks down: a deactivated window has to
1040          // be activated and then deactivated again to hide.
1041          var currentThreadId = PInvoke.GetCurrentThreadId();
1042          var foregroundThreadId = PInvoke.GetWindowThreadProcessId(foregroundWindow, null);
1043          if (foregroundThreadId != currentThreadId)
1044          {
1045              PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, true);
1046              PInvoke.SetForegroundWindow(_hwnd);
1047              PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, false);
1048          }
1049          else
1050          {
1051              PInvoke.SetForegroundWindow(_hwnd);
1052          }
1053      }
1054  }