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 }