Files
PowerToys/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
Michael Jolley 88238db78b Fix CmdPal startup and shutdown lifecycle races
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 7a42451c-0ec4-4778-86fd-c77d7ac9d122
2026-08-28 13:10:37 -05:00

2031 lines
80 KiB
C#

// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.InteropServices;
using CmdPalKeyboardService;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Messages;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Dock;
using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.Pages;
using Microsoft.CmdPal.UI.Services;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.ViewModels.Messages;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI;
using Microsoft.UI.Input;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Graphics;
using Windows.System;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging;
using WinUIEx;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
namespace Microsoft.CmdPal.UI;
public sealed partial class MainWindow : WindowEx,
IRecipient<DismissMessage>,
IRecipient<ShowWindowMessage>,
IRecipient<ShowPaletteAtMessage>,
IRecipient<HideWindowMessage>,
IRecipient<QuitMessage>,
IRecipient<NavigateToPageMessage>,
IRecipient<NavigationDepthMessage>,
IRecipient<SearchQueryMessage>,
IRecipient<ErrorOccurredMessage>,
IRecipient<DragStartedMessage>,
IRecipient<DragCompletedMessage>,
IRecipient<ToggleDevRibbonMessage>,
IRecipient<GetHwndMessage>,
IRecipient<ExpandCompactModeMessage>,
IRecipient<MaximizeForDialogMessage>,
IDisposable,
IHostWindow
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
private readonly uint WM_TASKBAR_RESTART;
private readonly HWND _hwnd;
private readonly DispatcherTimer _autoGoHomeTimer;
private readonly WNDPROC? _hotkeyWndProc;
private readonly WNDPROC? _originalWndProc;
private readonly List<TopLevelHotkey> _hotkeys = [];
private readonly KeyboardListener _keyboardListener;
private readonly LocalKeyboardListener _localKeyboardListener;
private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new();
private readonly ICmdPalProtocolActivation _protocolActivation;
private readonly ViewModels.Models.IMonitorService _monitorService;
private readonly IThemeService _themeService;
private readonly WindowThemeSynchronizer _windowThemeSynchronizer;
private readonly List<long> _breakthroughTimestamps = [];
private bool _ignoreHotKeyWhenFullScreen = true;
private bool _ignoreHotKeyWhenBusy;
private bool _allowBreakthroughShortcut;
private bool _suppressDpiChange;
private bool _themeServiceInitialized;
// The snapshot of settings last consumed by HotReloadSettings. Used to skip redundant
// hot-reloads when a SettingsChanged notification touches settings this window doesn't
// care about (theme, provider config, aliases, and so on).
private SettingsModel? _lastAppliedSettings;
// Session tracking for telemetry
private Stopwatch? _sessionStopwatch;
private int _sessionCommandsExecuted;
private int _sessionPagesVisited;
private int _sessionSearchQueriesCount;
private int _sessionMaxNavigationDepth;
private int _sessionErrorCount;
private bool _isUpdatingBackdrop;
private bool _isBackdropUpdatePending;
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
// Tracks the chrome mode currently applied to the HWND. Nullable so the first
// call to ApplyHwndFrameMode always runs, regardless of which mode we land in.
private bool? _hwndFrameVisible;
// Thickness (in DIPs) of the resize grip around the visible card's border. Shared
// by the InputNonClientPointerSource region registration (so WM_NCHITTEST actually
// fires over the border) and the WM_NCHITTEST handler (so it returns resize codes
// over the same band). These MUST match or the two disagree about where resizing is.
private const int ResizeBorderThicknessDip = 8;
private WindowPosition _currentWindowPosition = new();
private bool _preventHideWhenDeactivated;
private bool _isLoadedFromDock;
// While a modal dialog (e.g. a confirmation) is showing, the card is forced to fill the
// whole window so the dialog — which renders in the window's popup layer and is clipped to
// the card's HWND region — isn't cut off. Cleared when the dialog closes.
private bool _dialogFullExpandActive;
// The most recent expand/collapse request, remembered so the correct compact layout can be
// restored once a dialog-driven full expansion ends.
private bool _lastExpandRequested;
private DevRibbon? _devRibbon;
private MainWindowViewModel ViewModel { get; }
public bool IsVisibleToUser { get; private set; } = true;
public event EventHandler? IsVisibleToUserChanged;
public MainWindow()
{
_protocolActivation = App.Current.Services.GetRequiredService<ICmdPalProtocolActivation>();
_monitorService = App.Current.Services.GetRequiredService<ViewModels.Models.IMonitorService>();
InitializeComponent();
ViewModel = App.Current.Services.GetService<MainWindowViewModel>()!;
_autoGoHomeTimer = new DispatcherTimer();
_autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
_themeService = App.Current.Services.GetRequiredService<IThemeService>();
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
_windowThemeSynchronizer = new WindowThemeSynchronizer(_themeService, this);
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
unsafe
{
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
}
// The HWND itself is borderless / transparent — the visible card lives inside
// RootElement (CmdPalMainControl) and draws its own corners, border, shadow, and
// backdrop via the SystemBackdropElement. The frame can be re-enabled via an
// internal-only setting (hot-reloaded through HotReloadSettings) to make the
// HWND bounds visible while debugging.
var initialSettings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
ApplyHwndFrameMode(ShouldShowHwndFrame(initialSettings));
_hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached);
_keyboardListener = new KeyboardListener();
_keyboardListener.Start();
_keyboardListener.SetProcessCommand(new CmdPalKeyboardService.ProcessCommand(HandleSummon));
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
// member (and instead like, use a local), then the pointer we marshal
// into the WindowLongPtr will be useless after we leave this function,
// and our **WindProc will explode**.
_hotkeyWndProc = HotKeyPrc;
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
this.SetIcon();
AppWindow.Title = RS_.GetString("AppName");
RestoreWindowPositionFromSavedSettings();
UpdateWindowPositionInMemory();
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
WeakReferenceMessenger.Default.Register<ShowPaletteAtMessage>(this);
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
WeakReferenceMessenger.Default.Register<NavigationDepthMessage>(this);
WeakReferenceMessenger.Default.Register<SearchQueryMessage>(this);
WeakReferenceMessenger.Default.Register<ErrorOccurredMessage>(this);
WeakReferenceMessenger.Default.Register<DragStartedMessage>(this);
WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
WeakReferenceMessenger.Default.Register<ToggleDevRibbonMessage>(this);
WeakReferenceMessenger.Default.Register<GetHwndMessage>(this);
WeakReferenceMessenger.Default.Register<ExpandCompactModeMessage>(this);
WeakReferenceMessenger.Default.Register<MaximizeForDialogMessage>(this);
// Hide our titlebar.
// We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed
// to hide the old caption buttons. Then, in UpdateRegionsForCustomTitleBar,
// we'll make the top drag-able again. (after our content loads)
ExtendsContentIntoTitleBar = true;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
SizeChanged += WindowSizeChanged;
RootElement.Loaded += RootElementLoaded;
// Load our settings, and then also wire up a settings changed handler
HotReloadSettings();
App.Current.Services.GetRequiredService<ISettingsService>().SettingsChanged += SettingsChangedHandler;
// Make sure that we update the acrylic theme when the OS theme changes
RootElement.ActualThemeChanged += RootElement_ActualThemeChanged;
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
{
Summon(string.Empty);
});
_localKeyboardListener = new LocalKeyboardListener();
_localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
_localKeyboardListener.Start();
// Force window to be created, and then cloaked. This will offset initial animation when the window is shown.
HideWindow();
}
private void OnAutoGoHomeTimerOnTick(object? s, object e)
{
_autoGoHomeTimer.Stop();
// BEAR LOADING: Focus Search must be suppressed here; otherwise it may steal focus (for example, from the system tray icon)
// and prevent the user from opening its context menu.
WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false));
}
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
{
ScheduleBackdropUpdate();
}
private void RootElement_ActualThemeChanged(FrameworkElement sender, object args)
{
ScheduleBackdropUpdate();
}
private void ScheduleBackdropUpdate()
{
// A theme reload changes RequestedTheme several times to force WinUI to refresh
// its resources. Coalesce the resulting ThemeChanged / ActualThemeChanged events
// so the SystemBackdropElement is only updated once with the final theme.
if (_isBackdropUpdatePending)
{
return;
}
_isBackdropUpdatePending = true;
if (!DispatcherQueue.TryEnqueue(() =>
{
try
{
UpdateBackdrop();
}
finally
{
_isBackdropUpdatePending = false;
}
}))
{
_isBackdropUpdatePending = false;
}
}
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
{
if (e.Key == VirtualKey.GoBack)
{
WeakReferenceMessenger.Default.Send(new GoBackMessage());
}
}
private void SettingsChangedHandler(ISettingsService sender, SettingsModel args)
{
// Only rebuild window state when a setting HotReloadSettings actually consumes has
// changed. Most SettingsChanged notifications (theme, provider config, aliases, ...)
// don't affect the host window, so hot-reloading on every one is wasteful.
if (MainWindowSettingsComparer.Instance.Equals(_lastAppliedSettings, args))
{
return;
}
DispatcherQueue.TryEnqueue(HotReloadSettings);
}
private void RootElementLoaded(object sender, RoutedEventArgs e)
{
// Now that our content has loaded, we can update our draggable regions
UpdateRegionsForCustomTitleBar();
// Also update regions when DPI changes. SizeChanged only fires when the logical
// (DIP) size changes — a DPI change that scales the physical size while preserving
// the DIP size won't trigger it, leaving drag regions at the old physical coordinates.
RootElement.XamlRoot.Changed += XamlRoot_Changed;
// The visible card resizes inside the fixed-size HWND (e.g. compact <-> expanded),
// which does not raise WindowSizeChanged. Recompute the drag regions and the HWND
// clip region whenever the card's own size changes so they keep tracking it.
RootElement.CardElement.SizeChanged += CardElement_SizeChanged;
// Add dev ribbon if enabled. The ribbon lives inside the visible card so it
// doesn't draw into the transparent shadow area outside the rounded border.
if (!BuildInfo.IsCiBuild)
{
_devRibbon = new DevRibbon { Margin = new Thickness(-1, -1, 120, -1) };
RootElement.CardContentPanel.Children.Add(_devRibbon);
}
}
private void XamlRoot_Changed(XamlRoot sender, XamlRootChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
private void CardElement_SizeChanged(object sender, SizeChangedEventArgs e) => UpdateRegionsForCustomTitleBar();
private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
private void PositionCentered()
{
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest);
PositionCentered(displayArea);
}
private void PositionCentered(DisplayArea displayArea)
{
// Use the saved window size when available so that a dock-resized HWND
// (hidden but not destroyed) doesn't dictate the size on normal reopen.
SizeInt32 windowSize;
int windowDpi;
if (_currentWindowPosition.IsSizeValid)
{
windowSize = new SizeInt32(_currentWindowPosition.Width, _currentWindowPosition.Height);
windowDpi = _currentWindowPosition.Dpi;
}
else
{
windowSize = AppWindow.Size;
windowDpi = (int)this.GetDpiForWindow();
}
var rect = WindowPositionHelper.CenterOnDisplay(
displayArea,
windowSize,
windowDpi);
if (rect is not null)
{
var finalRect = rect.Value;
// In compact mode, center the *visible collapsed card* (the search box) on the
// display, not the much larger transparent HWND. The card is anchored to the top
// of the HWND, so we offset the HWND upward by the card's center so that growing
// the card downward (when results appear) keeps the search box where it was.
if (TryGetCompactCardCenterOffsetPhysical(windowDpi, out var cardCenterFromHwndTop))
{
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
var workArea = displayArea.WorkArea;
// The setting is the relative height measured from the *bottom* of the screen,
// so a larger percentage places the search box higher up the display.
var fractionFromTop = GetCompactCenterFractionFromTop(settings);
var desiredCardCenterY = workArea.Y + (int)Math.Round(workArea.Height * fractionFromTop);
finalRect.Y = desiredCardCenterY - cardCenterFromHwndTop;
if (finalRect.Y < workArea.Y)
{
finalRect.Y = workArea.Y;
}
}
MoveAndResizeDpiAware(finalRect);
}
}
/// <summary>
/// When the palette is in compact mode and is being centered on launch, computes the
/// distance (in physical pixels) from the top of the HWND to the vertical center of the
/// collapsed card, so the caller can position the HWND such that the card is centered.
/// Returns false when the card should not be re-centered (compact mode off, or a summon
/// behavior that restores the last position).
/// </summary>
private bool TryGetCompactCardCenterOffsetPhysical(int windowDpi, out int cardCenterFromHwndTop)
{
cardCenterFromHwndTop = 0;
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
if (!settings.CompactMode || !IsCenteringSummon(settings))
{
return false;
}
// Make sure the card is actually collapsed before we measure it.
(RootElement.MainContent as ShellPage)?.EnsureCompactLayout();
var cardHeightDip = RootElement.GetCardHeight();
if (cardHeightDip <= 0)
{
return false;
}
var scale = windowDpi / 96.0;
var cardTopDip = RootElement.ShadowPadding.Top;
cardCenterFromHwndTop = (int)Math.Round((cardTopDip + (cardHeightDip / 2.0)) * scale);
return true;
}
// Every summon behavior except ToLast centers the window on its target display.
private static bool IsCenteringSummon(SettingsModel settings) => settings.SummonOn != MonitorBehavior.ToLast;
// Converts the "center height" setting (a percentage measured up from the bottom of the
// screen) into the fraction of the work area, measured from the top, at which the
// collapsed search box should be centered.
private static double GetCompactCenterFractionFromTop(SettingsModel settings)
{
var pct = Math.Clamp(settings.CompactCenterHeightPercentage, 0, 100);
return 1.0 - (pct / 100.0);
}
private void RestoreWindowPosition(WindowPosition? savedPosition)
{
if (savedPosition?.IsSizeValid != true)
{
// don't try to restore if the saved position is invalid, just recenter
PositionCentered();
return;
}
var newRect = WindowPositionHelper.AdjustRectForVisibility(
savedPosition.ToPhysicalWindowRectangle(),
new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight),
savedPosition.Dpi);
MoveAndResizeDpiAware(newRect);
}
private void RestoreWindowPositionFromSavedSettings()
{
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
RestoreWindowPosition(settings?.LastWindowPosition);
}
private void RestoreWindowPositionFromMemory()
{
RestoreWindowPosition(_currentWindowPosition);
}
/// <summary>
/// Moves and resizes the window while suppressing WM_DPICHANGED.
/// The caller is expected to provide a rect already scaled for the target display's DPI.
/// Without suppression, the framework would apply its own DPI scaling on top, double-scaling the window.
/// </summary>
private void MoveAndResizeDpiAware(RectInt32 rect)
{
var originalMinHeight = MinHeight;
var originalMinWidth = MinWidth;
_suppressDpiChange = true;
try
{
// WindowEx is uses current DPI to calculate the minimum window size
MinHeight = 0;
MinWidth = 0;
AppWindow.MoveAndResize(rect);
}
finally
{
MinHeight = originalMinHeight;
MinWidth = originalMinWidth;
_suppressDpiChange = false;
}
}
private void UpdateWindowPositionInMemory()
{
var placement = new WINDOWPLACEMENT { length = (uint)Marshal.SizeOf<WINDOWPLACEMENT>() };
if (!PInvoke.GetWindowPlacement(_hwnd, ref placement))
{
return;
}
var rect = placement.rcNormalPosition;
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
// GetWindowPlacement returns rcNormalPosition in workspace coordinates for
// normal windows, but in screen coordinates for tool windows (WS_EX_TOOLWINDOW).
// HiddenOwnerWindowBehavior applies WS_EX_TOOLWINDOW to hide from taskbar/Alt+Tab,
// so we must check the current style before converting coordinates.
//
// To be on the safe side, we should consider the possibility that setting
// WS_EX_TOOLWINDOW failed or isn't applied while debugging.
var workArea = displayArea.WorkArea;
var isToolWindow = this.HasExtendedStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW);
_currentWindowPosition = new WindowPosition
{
X = rect.X + (isToolWindow ? 0 : workArea.X),
Y = rect.Y + (isToolWindow ? 0 : workArea.Y),
Width = rect.Width,
Height = rect.Height,
Dpi = (int)this.GetDpiForWindow(),
ScreenWidth = workArea.Width,
ScreenHeight = workArea.Height,
};
}
private void HotReloadSettings()
{
// NOTE: SettingsChangedHandler skips this method when nothing relevant changed, using
// MainWindowSettingsComparer. When you start consuming a new setting below, add it to
// that comparer too — otherwise changes to it won't trigger a hot-reload.
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
_lastAppliedSettings = settings;
SetupHotkey(settings);
App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
_ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
_ignoreHotKeyWhenBusy = settings.IgnoreShortcutWhenBusy;
_allowBreakthroughShortcut = settings.AllowBreakthroughShortcut;
_autoGoHomeInterval = settings.AutoGoHomeInterval;
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
ApplyHwndFrameMode(ShouldShowHwndFrame(settings));
// Start collapsed: the card shrinks to just the search box until there is a query.
HandleExpandCompactOnUiThread(false);
}
/// <summary>
/// Returns true if the user has opted in to seeing the OS-drawn HWND chrome (an internal
/// debugging setting). Always false in CI / release builds.
/// </summary>
private static bool ShouldShowHwndFrame(SettingsModel settings) =>
!BuildInfo.IsCiBuild && settings.ShowHwndFrame;
/// <summary>
/// Compares two <see cref="SettingsModel"/> instances by only the settings that
/// <see cref="HotReloadSettings"/> (and the methods it calls) actually consume. Any change
/// to a setting outside this set is invisible to the host window, so treating such models
/// as equal lets <see cref="SettingsChangedHandler"/> skip a redundant hot-reload.
/// </summary>
/// <remarks>
/// Keep this in sync with the settings read by <see cref="HotReloadSettings"/>,
/// <see cref="SetupHotkey"/>, <see cref="ShouldShowHwndFrame"/>, and
/// <see cref="HandleExpandCompactOnUiThread"/>.
/// </remarks>
private sealed class MainWindowSettingsComparer : IEqualityComparer<SettingsModel>
{
public static MainWindowSettingsComparer Instance { get; } = new();
public bool Equals(SettingsModel? x, SettingsModel? y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (x is null || y is null)
{
return false;
}
return x.UseLowLevelGlobalHotkey == y.UseLowLevelGlobalHotkey
&& x.ShowSystemTrayIcon == y.ShowSystemTrayIcon
&& x.IgnoreShortcutWhenFullscreen == y.IgnoreShortcutWhenFullscreen
&& x.IgnoreShortcutWhenBusy == y.IgnoreShortcutWhenBusy
&& x.AllowBreakthroughShortcut == y.AllowBreakthroughShortcut
&& x.AutoGoHomeInterval == y.AutoGoHomeInterval
&& x.ShowHwndFrame == y.ShowHwndFrame
&& x.CompactMode == y.CompactMode
&& x.Hotkey == y.Hotkey // HotkeySettings is a record (value equality)
&& CommandHotkeysEqual(x.CommandHotkeys, y.CommandHotkeys);
}
// TopLevelHotkey is a record (value equality); compare element-wise. ImmutableList
// itself only implements reference equality, so we can't rely on ==.
private static bool CommandHotkeysEqual(ImmutableList<TopLevelHotkey> x, ImmutableList<TopLevelHotkey> y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (x.Count != y.Count)
{
return false;
}
for (var i = 0; i < x.Count; i++)
{
if (x[i] != y[i])
{
return false;
}
}
return true;
}
public int GetHashCode(SettingsModel obj)
{
var hash = default(HashCode);
hash.Add(obj.UseLowLevelGlobalHotkey);
hash.Add(obj.ShowSystemTrayIcon);
hash.Add(obj.IgnoreShortcutWhenFullscreen);
hash.Add(obj.IgnoreShortcutWhenBusy);
hash.Add(obj.AllowBreakthroughShortcut);
hash.Add(obj.AutoGoHomeInterval);
hash.Add(obj.ShowHwndFrame);
hash.Add(obj.CompactMode);
hash.Add(obj.Hotkey);
hash.Add(obj.CommandHotkeys.Count);
return hash.ToHashCode();
}
}
/// <summary>
/// Configures the HWND for the borderless / transparent main-window mode and (when
/// the internal debug toggle is enabled) overlays the OS-drawn chrome so the HWND's
/// real bounds are easy to spot. Hit testing is always handled by
/// <see cref="HitTestForCardResize"/> — the frame flag is purely visual.
/// </summary>
private void ApplyHwndFrameMode(bool showFrame)
{
if (_hwndFrameVisible == showFrame)
{
return;
}
_hwndFrameVisible = showFrame;
// The HWND itself never paints — the card draws the backdrop. Re-applying this
// each toggle is safe (it just reassigns SystemBackdrop) and guards against the
// OS replacing it when chrome changes.
InitializeBackdropSupport();
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
{
// When the debug flag is off we hide the OS chrome (no title bar, no border).
// When on we let the OS draw both so the HWND outline is obvious.
// This must actually be applied (not just relied on via WM_NCCALCSIZE): now
// that the HWND is clipped to the card region, the OS-drawn title bar / frame
// is no longer covered by our full-window transparent content, so DWM would
// otherwise repaint it (most visibly the inactive caption) behind the card
// when the window loses focus.
overlappedPresenter.SetBorderAndTitleBar(showFrame, showFrame);
// IsResizable must stay true so WS_THICKFRAME is present. The OS only honors
// resize-style WM_NCHITTEST results (HTLEFT, HTRIGHT, HT{TOP,BOTTOM}{,LEFT,RIGHT})
// when the window has a sizing frame, even though we drive the resize from a
// custom NCHITTEST handler. Setting it after SetBorderAndTitleBar makes sure a
// borderless window still keeps its sizing frame.
overlappedPresenter.IsResizable = true;
}
ApplyHwndBorderAttributes(showFrame);
// Drag regions are computed relative to the visible card; the chrome change can
// shift its on-screen position, so refresh.
UpdateRegionsForCustomTitleBar();
}
/// <summary>
/// Applies the DWM corner and border attributes for the current frame mode. This is
/// split out from <see cref="ApplyHwndFrameMode"/> because the DWM border color does
/// not reliably "take" when first set during window construction (before the HWND has
/// been shown on a cold process start) — leaving the faint OS outline visible until
/// the chrome is toggled. Re-applying it each time the window is shown guarantees the
/// borderless look on a cold start.
/// </summary>
private void ApplyHwndBorderAttributes(bool showFrame)
{
unsafe
{
// Rounded corners: let the OS pick when the debug frame is on, suppress
// otherwise so the card's CornerRadius isn't doubled by an OS rounding.
var corner = (uint)(showFrame
? DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DEFAULT
: DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DONOTROUND);
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, &corner, sizeof(uint));
// DWMWA_BORDER_COLOR: 0xFFFFFFFE = DWMWA_COLOR_NONE (no border drawn);
// 0xFFFFFFFF = DWMWA_COLOR_DEFAULT (system default). With WS_THICKFRAME still
// on, DWM otherwise draws a faint 1px outline around the HWND — which the
// user sees as the "frame still appears around the sides" even when our
// ShowHwndFrame setting is off. Setting COLOR_NONE removes it.
const uint DWMWA_COLOR_NONE = 0xFFFFFFFEu;
const uint DWMWA_COLOR_DEFAULT = 0xFFFFFFFFu;
var borderColor = showFrame ? DWMWA_COLOR_DEFAULT : DWMWA_COLOR_NONE;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_BORDER_COLOR, &borderColor, sizeof(uint));
}
RedrawWindow(_hwnd);
}
private void InitializeBackdropSupport()
{
// The window itself paints nothing (it's transparent). All actual backdrop
// rendering lives on the SystemBackdropElement inside CmdPalMainControl, so the
// mica/acrylic only fills the rounded card instead of the whole HWND. The empty
// tint here keeps the HWND fully transparent.
SystemBackdrop = new TransparentTintBackdrop { TintColor = Colors.Transparent };
}
private void UpdateBackdrop()
{
// Prevent re-entrance when backdrop changes trigger ActualThemeChanged
if (_isUpdatingBackdrop)
{
return;
}
_isUpdatingBackdrop = true;
try
{
var backdrop = _themeService.Current.BackdropParameters;
var isImageMode = ViewModel.ShowBackgroundImage;
var config = BackdropStyles.Get(backdrop.Style);
var hasColorization = _themeService.Current.HasColorization;
RootElement.ApplyBackdrop(backdrop, config.ControllerKind, isImageMode, hasColorization);
}
catch (Exception ex)
{
Logger.LogError("Failed to update backdrop", ex);
}
finally
{
_isUpdatingBackdrop = false;
}
}
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
{
var positionWindowForTargetMonitor = (HWND hwnd) =>
{
if (target == MonitorBehavior.ToLast)
{
var originalScreen = new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight);
var newRect = WindowPositionHelper.AdjustRectForVisibility(_currentWindowPosition.ToPhysicalWindowRectangle(), originalScreen, _currentWindowPosition.Dpi);
MoveAndResizeDpiAware(newRect);
}
else
{
var display = GetScreen(hwnd, target);
PositionCentered(display);
}
};
ShowHwnd(hwndValue, positionWindowForTargetMonitor);
}
private void ShowHwnd(IntPtr hwndValue, Point anchorInPixels, AnchorPoint anchorCorner)
{
var positionWindowForAnchor = (HWND hwnd) =>
{
PInvoke.GetWindowRect(hwnd, out var bounds);
var swpFlags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED;
switch (anchorCorner)
{
case AnchorPoint.TopLeft:
PInvoke.SetWindowPos(
hwnd,
HWND.HWND_TOP,
(int)anchorInPixels.X,
(int)anchorInPixels.Y,
0,
0,
swpFlags);
break;
case AnchorPoint.TopRight:
PInvoke.SetWindowPos(
hwnd,
HWND.HWND_TOP,
(int)(anchorInPixels.X - bounds.Width),
(int)anchorInPixels.Y,
0,
0,
swpFlags);
break;
case AnchorPoint.BottomLeft:
PInvoke.SetWindowPos(
hwnd,
HWND.HWND_TOP,
(int)anchorInPixels.X,
(int)(anchorInPixels.Y - bounds.Height),
0,
0,
swpFlags);
break;
case AnchorPoint.BottomRight:
PInvoke.SetWindowPos(
hwnd,
HWND.HWND_TOP,
(int)(anchorInPixels.X - bounds.Width),
(int)(anchorInPixels.Y - bounds.Height),
0,
0,
swpFlags);
break;
}
};
ShowHwnd(hwndValue, positionWindowForAnchor);
}
private void ShowHwnd(IntPtr hwndValue, Action<HWND>? positionWindow)
{
StopAutoGoHome();
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
// Remember, IsIconic == "minimized", which is entirely different state
// from "show/hide"
// If we're currently minimized, restore us first, before we reveal
// our window. Otherwise, we'd just be showing a minimized window -
// which would remain not visible to the user.
if (PInvoke.IsIconic(hwnd))
{
// Make sure our HWND is cloaked before any possible window manipulations
Cloak();
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
}
if (positionWindow is not null)
{
positionWindow(hwnd);
}
// Check if the debugger is attached. If it is, we don't want to apply the tool window style,
// because that would make it hard to debug the app
if (Debugger.IsAttached)
{
_hiddenOwnerBehavior.ShowInTaskbar(this, true);
}
// Just to be sure, SHOW our hwnd.
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_SHOW);
// Re-apply the borderless DWM attributes now that the window is
// actually shown. On a cold launch these are first set during
// construction before the HWND has ever been displayed, and DWM doesn't
// reliably honor the border color until the window exists on-screen —
// which left the faint OS outline visible until the chrome was toggled.
ApplyHwndBorderAttributes(_hwndFrameVisible ?? false);
// Once we're done, uncloak to avoid all animations
Uncloak();
PInvoke.SetForegroundWindow(hwnd);
PInvoke.SetActiveWindow(hwnd);
// Push our window to the top of the Z-order and make it the topmost, so
// that it appears above all other windows. We want to remove the
// topmost status when we hide the window (because we cloak it instead
// of hiding it).
//
// SWP_FRAMECHANGED re-sends WM_NCCALCSIZE so the OS recomputes the
// (zero-width) frame. But on this cloak->show path it doesn't reliably
// *repaint* the non-client area, so the WS_THICKFRAME border that was
// painted earlier survives on the top/left/right edges (the bottom is
// clipped away by the card region). A real resize fixes it because it
// forces that NC repaint - so we do the same explicitly below.
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED);
// Force the non-client frame to actually redraw (RDW_FRAME) and the
// client to repaint over wherever it used to be. Without this the stale
// border lingers until the user resizes the window.
RedrawWindow(hwnd);
// Treat the overall show/hide lifecycle as the authoritative
// visibility transition, not the lower-level cloak/uncloak helpers.
SetIsVisibleToUser(true);
}
private static void RedrawWindow(HWND hwnd)
{
const uint RDW_INVALIDATE = 0x0001;
const uint RDW_UPDATENOW = 0x0100;
const uint RDW_ALLCHILDREN = 0x0080;
const uint RDW_FRAME = 0x0400;
_ = RedrawWindow(hwnd, IntPtr.Zero, IntPtr.Zero, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN | RDW_FRAME);
}
private static DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
{
// Leaving a note here, in case we ever need it:
// https://github.com/microsoft/microsoft-ui-xaml/issues/6454
// If we need to ever FindAll, we'll need to iterate manually
var displayAreas = Microsoft.UI.Windowing.DisplayArea.FindAll();
switch (target)
{
case MonitorBehavior.InPlace:
if (PInvoke.GetWindowRect(currentHwnd, out var bounds))
{
RectInt32 converted = new(bounds.X, bounds.Y, bounds.Width, bounds.Height);
return DisplayArea.GetFromRect(converted, DisplayAreaFallback.Nearest);
}
break;
case MonitorBehavior.ToFocusedWindow:
var foregroundWindowHandle = PInvoke.GetForegroundWindow();
if (foregroundWindowHandle != IntPtr.Zero)
{
if (PInvoke.GetWindowRect(foregroundWindowHandle, out var fgBounds))
{
RectInt32 converted = new(fgBounds.X, fgBounds.Y, fgBounds.Width, fgBounds.Height);
return DisplayArea.GetFromRect(converted, DisplayAreaFallback.Nearest);
}
}
break;
case MonitorBehavior.ToPrimary:
return DisplayArea.Primary;
case MonitorBehavior.ToMouse:
default:
if (PInvoke.GetCursorPos(out var cursorPos))
{
return DisplayArea.GetFromPoint(new PointInt32(cursorPos.X, cursorPos.Y), DisplayAreaFallback.Nearest);
}
break;
}
return DisplayArea.Primary;
}
public void Receive(ShowWindowMessage message)
{
_isLoadedFromDock = false;
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
// Start session tracking
_sessionStopwatch = Stopwatch.StartNew();
_sessionCommandsExecuted = 0;
_sessionPagesVisited = 0;
ShowHwnd(message.Hwnd, settings.SummonOn);
}
internal void Receive(ShowPaletteAtMessage message)
{
_isLoadedFromDock = true;
// Reset the size in case users have resized a dock window.
// Ideally in the future, we'll have defined sizes that opening
// a dock window will adhere to, but alas, that's the future.
RestoreWindowPositionFromMemory();
ShowHwnd(HWND.Null, message.PosPixels, message.Anchor);
}
public void Receive(HideWindowMessage message)
{
// This might come in off the UI thread. Make sure to hop back.
DispatcherQueue.TryEnqueue(() =>
{
EndSession("Hide");
HideWindow();
});
}
public void Receive(QuitMessage message) =>
// This might come in on a background thread
DispatcherQueue.TryEnqueue(() => Close());
public void Receive(DismissMessage message)
{
if (message.ForceGoHome)
{
WeakReferenceMessenger.Default.Send(new GoHomeMessage(false, false));
}
// This might come in off the UI thread. Make sure to hop back.
DispatcherQueue.TryEnqueue(() =>
{
EndSession("Dismiss");
HideWindow();
});
}
// Session telemetry: Track metrics during the Command Palette session
// These receivers increment counters that are sent when EndSession is called
public void Receive(NavigateToPageMessage message)
{
_sessionPagesVisited++;
}
public void Receive(NavigationDepthMessage message)
{
if (message.Depth > _sessionMaxNavigationDepth)
{
_sessionMaxNavigationDepth = message.Depth;
}
}
public void Receive(SearchQueryMessage message)
{
_sessionSearchQueriesCount++;
}
public void Receive(ErrorOccurredMessage message)
{
_sessionErrorCount++;
}
/// <summary>
/// Ends the current telemetry session and emits the CmdPal_SessionDuration event.
/// Aggregates all session metrics collected since ShowWindow and sends them to telemetry.
/// </summary>
/// <param name="dismissalReason">The reason the session ended (e.g., Dismiss, Hide, LostFocus).</param>
private void EndSession(string dismissalReason)
{
if (_sessionStopwatch is not null)
{
_sessionStopwatch.Stop();
TelemetryForwarder.LogSessionDuration(
(ulong)_sessionStopwatch.ElapsedMilliseconds,
_sessionCommandsExecuted,
_sessionPagesVisited,
dismissalReason,
_sessionSearchQueriesCount,
_sessionMaxNavigationDepth,
_sessionErrorCount);
_sessionStopwatch = null;
}
}
/// <summary>
/// Increments the session commands executed counter for telemetry.
/// Called by TelemetryForwarder when an extension command is invoked.
/// </summary>
internal void IncrementCommandsExecuted()
{
_sessionCommandsExecuted++;
}
private void HideWindow()
{
// Cloak our HWND to avoid all animations.
var cloaked = Cloak();
// Then hide our HWND, to make sure that the OS gives the FG / focus back to another app
// (there's no way for us to guess what the right hwnd might be, only the OS can do it right)
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
if (cloaked)
{
// TRICKY: show our HWND again. This will trick XAML into painting our
// HWND again, so that we avoid the "flicker" caused by a WinUI3 app
// window being first shown
// SW_SHOWNA will prevent us for trying to fight the focus back
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_SHOWNA);
// Intentionally leave the window cloaked. So our window is "visible",
// but also cloaked, so you can't see it.
// If the window was not cloaked, then leave it hidden.
// Sure, it's not ideal, but at least it's not visible.
}
// Treat the overall show/hide lifecycle as the authoritative
// visibility transition, not the lower-level cloak/uncloak helpers.
SetIsVisibleToUser(false);
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
// Start auto-go-home timer
RestartAutoGoHome();
}
private void StopAutoGoHome()
{
_autoGoHomeTimer.Stop();
}
private void RestartAutoGoHome()
{
if (_autoGoHomeInterval == Timeout.InfiniteTimeSpan)
{
return;
}
_autoGoHomeTimer.Stop();
_autoGoHomeTimer.Start();
}
private bool Cloak()
{
bool wasCloaked;
unsafe
{
BOOL value = true;
var hr = PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
if (hr.Failed)
{
Logger.LogWarning($"DWM cloaking of the main window failed. HRESULT: {hr.Value}.");
}
wasCloaked = hr.Succeeded;
}
return wasCloaked;
}
private void Uncloak()
{
unsafe
{
BOOL value = false;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
}
}
private void SetIsVisibleToUser(bool isVisibleToUser)
{
if (IsVisibleToUser == isVisibleToUser)
{
return;
}
IsVisibleToUser = isVisibleToUser;
IsVisibleToUserChanged?.Invoke(this, EventArgs.Empty);
}
internal void MainWindow_Closed(object sender, WindowEventArgs args)
{
var serviceProvider = App.Current.Services;
if (!_isLoadedFromDock)
{
UpdateWindowPositionInMemory();
}
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
if (settings is not null)
{
// If we were last shown from the dock, _currentWindowPosition still holds
// the last non-dock placement because dock sessions intentionally skip updates.
if (_currentWindowPosition.IsSizeValid)
{
settingsService.UpdateSettings(s => s with { LastWindowPosition = _currentWindowPosition });
}
}
App.Current.Services.GetService<TrayIconService>()!.Destroy();
// WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592).
// Workaround by turning it off before shutdown.
App.Current.DebugSettings.FailFastOnErrors = false;
_localKeyboardListener.Dispose();
DisposeAcrylic();
_keyboardListener.Stop();
App.Current.RequestProcessExit();
}
private void DisposeAcrylic()
{
// Backdrop resources are thread-affine. ClearBackdrop closes the active controller or
// brush on the XAML thread, but leaves SystemBackdrop assigned so its target stays rooted.
// Clearing it can let C#/WinRT finalize ContentExternalBackdropLink off-thread, which
// fail-fasts with RPC_E_WRONG_THREAD (0x8001010E).
try
{
RootElement?.ClearBackdrop();
}
catch
{
// Best-effort cleanup; ignore errors during shutdown.
}
}
// Updates our window s.t. the top of the window is draggable.
private void UpdateRegionsForCustomTitleBar()
{
var xamlRoot = RootElement.XamlRoot;
if (xamlRoot is null)
{
return;
}
// Specify the interactive regions of the title bar.
var scaleAdjustment = xamlRoot.RasterizationScale;
// Drag/passthrough regions are computed against the visible card (the rounded
// border inside CmdPalMainControl), not the whole HWND. The HWND extends beyond
// the card to make room for the drop shadow, and we don't want that transparent
// shadow area to be draggable.
var card = RootElement.CardElement;
if (card.ActualWidth <= 0 || card.ActualHeight <= 0)
{
return;
}
// All coordinates below are in the card's own (DIP) space: (0,0) is the
// top-left of the visible card, (w,h) is the bottom-right. GetRect transforms
// them into the physical-pixel client coordinates that
// InputNonClientPointerSource expects.
var transform = card.TransformToVisual(null);
var w = card.ActualWidth;
var h = card.ActualHeight;
RectInt32 CardRect(double x, double y, double rw, double rh) =>
GetRect(transform.TransformBounds(new Rect(x, y, rw, rh)), scaleAdjustment);
// Reserve some space at the top for dragging the window (caption).
const double dragHeight = 16;
// The resize grip straddles each card edge by `grip` DIPs on either side so the
// affordance reaches a little into the drop-shadow padding too.
const double grip = ResizeBorderThicknessDip;
var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(this.AppWindow.Id);
// Mark the card's border ring + top drag bar as Caption. Only regions
// registered here generate WM_NCHITTEST on our wndproc - without the
// side/bottom strips the XAML island swallows the pointer and we never
// get a resize. HotKeyPrc's WM_NCHITTEST then picks drag vs. resize
// per-pixel.
var caption = new RectInt32[]
{
CardRect(0, 0, w, dragHeight), // top drag bar
CardRect(-grip, -grip, 2 * grip, h + (2 * grip)), // left edge
CardRect(w - grip, -grip, 2 * grip, h + (2 * grip)), // right edge
CardRect(-grip, -grip, w + (2 * grip), 2 * grip), // top edge
CardRect(-grip, h - grip, w + (2 * grip), 2 * grip), // bottom edge
};
nonClientInputSrc.SetRegionRects(NonClientRegionKind.Caption, caption);
// Everything inside the border ring (and below the drag bar) is
// interactive content. Marking it Passthrough keeps the search box,
// list, etc. clickable and explicitly carves it out of the caption
// regions above.
var interiorWidth = Math.Max(0, w - (2 * grip));
var interiorHeight = Math.Max(0, h - dragHeight - grip);
var passthrough = new RectInt32[]
{
CardRect(grip, dragHeight, interiorWidth, interiorHeight),
};
nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, passthrough);
// Clip the HWND to the card + its shadow. Card is inset by
// ShadowPadding on each side, so card + full padding lands flush with
// the HWND edge - and a region flush with the edge makes WS_THICKFRAME
// draw its border there. So inset 1px on every side to hide that.
// Bottom is clamped to 1px inside the HWND so the border doesn't
// reappear as the card grows tall.
var shadowPadding = RootElement.ShadowPadding;
var cardPhysical = CardRect(0, 0, w, h);
const int EdgeInsetPx = 1;
var windowWidthPx = AppWindow.Size.Width;
var windowHeightPx = AppWindow.Size.Height;
var clipLeft = EdgeInsetPx;
var clipTop = EdgeInsetPx;
var clipRight = windowWidthPx - EdgeInsetPx;
var bottomShadowPx = (int)Math.Round(shadowPadding.Bottom * scaleAdjustment);
var clipBottom = Math.Min(
cardPhysical.Y + cardPhysical.Height + bottomShadowPx,
windowHeightPx - EdgeInsetPx);
ApplyCardWindowRegion(new RectInt32(
clipLeft,
clipTop,
Math.Max(0, clipRight - clipLeft),
Math.Max(0, clipBottom - clipTop)));
}
/// <summary>
/// Restricts the HWND's visible / hit-testable area to the supplied
/// rectangle (in physical client pixels), which covers the visible card and
/// its drop-shadow margin. Everything outside — the empty transparent area
/// of the (larger) HWND — becomes click-through and is excluded from the
/// window region. When the debug HWND frame is enabled the clip is removed
/// so the full window stays visible.
/// </summary>
private void ApplyCardWindowRegion(RectInt32 regionPhysical)
{
nint hwnd;
unsafe
{
hwnd = (nint)_hwnd.Value;
}
// Debug frame mode: keep the whole window visible / interactive, no clip.
if (_hwndFrameVisible == true)
{
_ = SetWindowRgn(hwnd, IntPtr.Zero, true);
return;
}
// CreateRectRgn coordinates are relative to the window's top-left. For this
// borderless popup the client origin coincides with the window origin, so the
// region's client-space physical rect maps directly into window space.
var region = CreateRectRgn(
regionPhysical.X,
regionPhysical.Y,
regionPhysical.X + regionPhysical.Width,
regionPhysical.Y + regionPhysical.Height);
if (region == IntPtr.Zero)
{
return;
}
// On success SetWindowRgn takes ownership of the region (the OS frees it), so we
// only delete it ourselves if the call failed.
if (SetWindowRgn(hwnd, region, true) == 0)
{
_ = DeleteObject(region);
}
}
private static RectInt32 GetRect(Rect bounds, double scale)
{
return new RectInt32(
_X: (int)Math.Round(bounds.X * scale),
_Y: (int)Math.Round(bounds.Y * scale),
_Width: (int)Math.Round(bounds.Width * scale),
_Height: (int)Math.Round(bounds.Height * scale));
}
// Raw interop for the window-region clip. Declared here (rather than via CsWin32)
// because SetWindowRgn transfers ownership of the HRGN to the OS on success, which is
// awkward to express through CsWin32's SafeHandle-returning region creator.
[DllImport("user32.dll", SetLastError = true)]
private static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, [MarshalAs(UnmanagedType.Bool)] bool bRedraw);
[DllImport("gdi32.dll")]
private static extern IntPtr CreateRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeleteObject(IntPtr hObject);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, uint flags);
internal void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
if (!_themeServiceInitialized && args.WindowActivationState != WindowActivationState.Deactivated)
{
try
{
_themeService.Initialize();
_themeServiceInitialized = true;
}
catch (Exception ex)
{
Logger.LogError("Failed to initialize ThemeService", ex);
}
}
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
// Save the current window position before hiding the window
// but not when opened from dock — preserve the pre-dock size.
if (!_isLoadedFromDock)
{
UpdateWindowPositionInMemory();
}
// If there's a debugger attached...
if (System.Diagnostics.Debugger.IsAttached)
{
// ... then don't hide the window when it loses focus.
return;
}
// Are we disabled? If we are, then we don't want to dismiss on focus lost.
// This can happen if an extension wanted to show a modal dialog on top of our
// window i.e. in the case of an MSAL auth window.
if (PInvoke.IsWindowEnabled(_hwnd) == 0)
{
return;
}
// We're doing something that requires us to lose focus, but we don't want to hide the window
if (_preventHideWhenDeactivated)
{
return;
}
// This will DWM cloak our window:
EndSession("LostFocus");
HideWindow();
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
}
if (RootElement is not null)
{
RootElement.SetIsInputActive(args.WindowActivationState != WindowActivationState.Deactivated);
}
}
public void HandleLaunchNonUI(AppActivationArguments? activatedEventArgs)
{
// LOAD BEARING
// Any reading and processing of the activation arguments must be done
// synchronously in this method, before it returns. The sending instance
// remains blocked until this returns; afterward it may quit, causing
// the activation arguments to be lost.
if (activatedEventArgs is null)
{
Summon(string.Empty);
return;
}
try
{
if (activatedEventArgs.Kind == ExtendedActivationKind.StartupTask)
{
return;
}
if (activatedEventArgs.Kind == ExtendedActivationKind.Protocol)
{
if (activatedEventArgs.Data is IProtocolActivatedEventArgs protocolArgs &&
_protocolActivation.TryParse(protocolArgs.Uri, out var route))
{
switch (route)
{
case CmdPalProtocolRoute.Background:
// we're running, we don't want to activate our window. bail
return;
case CmdPalProtocolRoute.OpenSettings openSettings:
WeakReferenceMessenger.Default.Send(openSettings.Message);
return;
case CmdPalProtocolRoute.Reload:
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
if (settings?.AllowExternalReload == true)
{
Logger.LogInfo("External Reload triggered");
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
}
else
{
Logger.LogInfo("External Reload is disabled");
}
return;
}
}
}
}
catch (COMException ex)
{
// https://learn.microsoft.com/en-us/windows/win32/rpc/rpc-return-values
const int RPC_S_SERVER_UNAVAILABLE = -2147023174;
const int RPC_S_CALL_FAILED = 2147023170;
// Accessing properties activatedEventArgs.Kind and activatedEventArgs.Data might cause COMException
// if the args are not valid or not passed correctly.
if (ex.HResult is RPC_S_SERVER_UNAVAILABLE or RPC_S_CALL_FAILED)
{
Logger.LogWarning(
$"COM exception (HRESULT {ex.HResult}) when accessing activation arguments. " +
$"This might be due to the calling application not passing them correctly or exiting before we could read them. " +
$"The application will continue running and fall back to showing the Command Palette window.");
}
else
{
Logger.LogError(
$"COM exception (HRESULT {ex.HResult}) when activating the application. " +
$"The application will continue running and fall back to showing the Command Palette window.",
ex);
}
}
Summon(string.Empty);
}
public void Summon(string commandId) =>
// The actual showing and hiding of the window will be done by the
// ShellPage. This is because we don't want to show the window if the
// user bound a hotkey to just an invokable command, which we can't
// know till the message is being handled.
WeakReferenceMessenger.Default.Send<HotkeySummonMessage>(new(commandId, _hwnd));
private void UnregisterHotkeys()
{
_keyboardListener.ClearHotkeys();
while (_hotkeys.Count > 0)
{
PInvoke.UnregisterHotKey(_hwnd, _hotkeys.Count - 1);
_hotkeys.RemoveAt(_hotkeys.Count - 1);
}
}
private void SetupHotkey(SettingsModel settings)
{
UnregisterHotkeys();
var globalHotkey = settings.Hotkey;
if (globalHotkey is not null)
{
if (settings.UseLowLevelGlobalHotkey)
{
_keyboardListener.SetHotkeyAction(globalHotkey.Win, globalHotkey.Ctrl, globalHotkey.Shift, globalHotkey.Alt, (byte)globalHotkey.Code, string.Empty);
_hotkeys.Add(new(globalHotkey, string.Empty));
}
else
{
var vk = globalHotkey.Code;
var modifiers =
(globalHotkey.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) |
(globalHotkey.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) |
(globalHotkey.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) |
(globalHotkey.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0)
;
var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk);
if (success)
{
_hotkeys.Add(new(globalHotkey, string.Empty));
}
}
}
foreach (var commandHotkey in settings.CommandHotkeys)
{
var key = commandHotkey.Hotkey;
if (key is not null)
{
if (settings.UseLowLevelGlobalHotkey)
{
_keyboardListener.SetHotkeyAction(key.Win, key.Ctrl, key.Shift, key.Alt, (byte)key.Code, commandHotkey.CommandId);
_hotkeys.Add(new(globalHotkey, string.Empty));
}
else
{
var vk = key.Code;
var modifiers =
(key.Alt ? HOT_KEY_MODIFIERS.MOD_ALT : 0) |
(key.Ctrl ? HOT_KEY_MODIFIERS.MOD_CONTROL : 0) |
(key.Shift ? HOT_KEY_MODIFIERS.MOD_SHIFT : 0) |
(key.Win ? HOT_KEY_MODIFIERS.MOD_WIN : 0)
;
var success = PInvoke.RegisterHotKey(_hwnd, _hotkeys.Count, modifiers, (uint)vk);
if (success)
{
_hotkeys.Add(commandHotkey);
}
}
}
}
}
private void HandleSummon(string commandId)
{
var isRootHotkey = string.IsNullOrEmpty(commandId);
if (isRootHotkey && IsPaletteVisibleToUser())
{
HandleSummonCore(commandId);
return;
}
var notificationFlags = WindowHelper.GetUserNotificationFlags();
var shouldSuppress =
(_ignoreHotKeyWhenFullScreen && notificationFlags.IsFullscreenState) ||
(_ignoreHotKeyWhenBusy && notificationFlags.IsBusy);
if (shouldSuppress)
{
if (_allowBreakthroughShortcut && IsBreakthroughTriggered())
{
// Rapid-press breakthrough: let it through
}
else
{
return;
}
}
HandleSummonCore(commandId);
}
private bool IsPaletteVisibleToUser()
{
var isVisible = Visible;
unsafe
{
// We need to check if our window is cloaked or not. A cloaked window is still
// technically visible, because SHOW/HIDE != iconic (minimized) != cloaked
// (these are all separate states)
long attr = 0;
PInvoke.DwmGetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAKED, &attr, sizeof(long));
if (attr == 1 /* DWM_CLOAKED_APP */)
{
isVisible = false;
}
}
return isVisible;
}
private bool IsBreakthroughTriggered()
{
const int requiredPresses = 3;
var windowTicks = 2 * Stopwatch.Frequency; // 2 seconds
var now = Stopwatch.GetTimestamp();
_breakthroughTimestamps.Add(now);
// Prune timestamps outside the window
_breakthroughTimestamps.RemoveAll(t => now - t > windowTicks);
if (_breakthroughTimestamps.Count >= requiredPresses)
{
_breakthroughTimestamps.Clear();
return true;
}
return false;
}
private void HandleSummonCore(string commandId)
{
var isRootHotkey = string.IsNullOrEmpty(commandId);
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
var isVisible = IsPaletteVisibleToUser();
// Note to future us: the wParam will have the index of the hotkey we registered.
// We can use that in the future to differentiate the hotkeys we've pressed
// so that we can bind hotkeys to individual commands
if (!isVisible || !isRootHotkey)
{
Summon(commandId);
}
else if (isRootHotkey)
{
// If there's a debugger attached...
if (System.Diagnostics.Debugger.IsAttached)
{
// ... then manually hide our window. When debugged, we won't get the cool cloaking,
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
Cloak();
this.Hide();
SetIsVisibleToUser(false);
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
return;
}
HideWindow();
}
}
private LRESULT HotKeyPrc(
HWND hwnd,
uint uMsg,
WPARAM wParam,
LPARAM lParam)
{
switch (uMsg)
{
// Prevent the window from maximizing when double-clicking the title bar area
case PInvoke.WM_NCLBUTTONDBLCLK:
return (LRESULT)IntPtr.Zero;
// When restoring a saved position across monitors with different DPIs,
// MoveAndResize already sets the correctly-scaled size. Suppress the
// framework's automatic DPI resize to avoid double-scaling.
case PInvoke.WM_DPICHANGED when _suppressDpiChange:
return (LRESULT)IntPtr.Zero;
case PInvoke.WM_NCHITTEST:
{
var ht = HitTestForCardResize(lParam);
if (ht != 0)
{
return (LRESULT)(nint)ht;
}
break;
}
// Borderless mode: claim the entire window rectangle as client area.
// A resizable window has WS_THICKFRAME, which makes the OS reserve a
// non-client sizing frame *and* gives the window a DWM drop shadow / a thin
// frame line along the top. We keep WS_THICKFRAME (so our custom WM_NCHITTEST
// can still drive resizing) but tell the OS the whole window is client by
// returning 0 from WM_NCCALCSIZE — which removes that frame and its shadow.
// The visible card draws its own border + shadow inside the transparent HWND.
// When the debug frame is on we fall through to the default handling so the
// real OS chrome appears.
case PInvoke.WM_NCCALCSIZE when wParam.Value != 0 && _hwndFrameVisible != true:
return (LRESULT)0;
// On every activation change the OS repaints the non-client frame to
// flip between the active / inactive caption. With WS_THICKFRAME still
// present that paints the OS border back over our borderless window each
// time we lose (or gain) focus - which is the frame that reappears when
// another window steals focus, and the one that shows up shortly after
// startup (the first activation flip). Passing -1 as the update-region
// (lParam) to DefWindowProc keeps the correct activation result while
// telling it to skip the non-client repaint entirely.
case PInvoke.WM_NCACTIVATE when _hwndFrameVisible != true:
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, new LPARAM(-1));
case PInvoke.WM_SYSCOMMAND:
{
var command = (int)(wParam.Value & 0xFFF0);
if (command == PInvoke.SC_CLOSE)
{
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
if (settings.AllowAltF4)
{
WeakReferenceMessenger.Default.Send<QuitMessage>();
}
return (LRESULT)IntPtr.Zero;
}
break;
}
case PInvoke.WM_HOTKEY:
{
var hotkeyIndex = (int)wParam.Value;
if (hotkeyIndex < _hotkeys.Count)
{
var hotkey = _hotkeys[hotkeyIndex];
HandleSummon(hotkey.CommandId);
}
return (LRESULT)IntPtr.Zero;
}
// Unlike DockWindow instances, MainWindow always exists, so it's the one
// reliable place to catch topology changes. Without this, the Settings page's
// monitor list goes stale whenever no dock window is around to see WM_DISPLAYCHANGE.
case PInvoke.WM_DISPLAYCHANGE:
Logger.LogDebug("MainWindow WM_DISPLAYCHANGE");
_monitorService.NotifyMonitorsChanged();
break;
default:
if (uMsg == WM_TASKBAR_RESTART)
{
HotReloadSettings();
}
break;
}
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
}
/// <summary>
/// Custom WM_NCHITTEST handler that turns the visible card's border (the rounded
/// stroke drawn by <see cref="CmdPalMainControl"/>) into the window's resize handles.
/// Without this the borderless / transparent HWND has no visible resize affordance,
/// even though the OS still allows resizing along the (invisible) HWND edges.
/// </summary>
/// <returns>
/// A non-zero HT* value to override the system hit test, or 0 to fall through to
/// the default WndProc (which lets the InputNonClientPointerSource Caption /
/// Passthrough regions decide caption vs. client behavior inside the card).
/// </returns>
private uint HitTestForCardResize(LPARAM lParam)
{
// NB: We intentionally do *not* short-circuit when the debug frame is showing.
// The HWND frame toggle is purely a visual diagnostic; resize hit-testing
// remains ours in both modes so the card's border is always the grab area.
if (RootElement is null || RootElement.XamlRoot is null)
{
return 0;
}
// LPARAM packs the screen-space pointer position: low word = x, high word = y,
// both as signed 16-bit ints.
var ptX = (short)(lParam.Value & 0xFFFF);
var ptY = (short)((lParam.Value >> 16) & 0xFFFF);
if (!PInvoke.GetWindowRect(_hwnd, out var windowRect))
{
return 0;
}
// Convert the card's ShadowPadding (DIPs) into screen pixels so we can locate
// the visible card rect within the (larger, transparent) HWND.
var dpi = PInvoke.GetDpiForWindow(_hwnd);
var scale = dpi / 96.0;
var padding = RootElement.ShadowPadding;
var cardLeft = windowRect.left + (int)Math.Round(padding.Left * scale);
var cardTop = windowRect.top + (int)Math.Round(padding.Top * scale);
var cardRight = windowRect.right - (int)Math.Round(padding.Right * scale);
var cardBottom = windowRect.bottom - (int)Math.Round(padding.Bottom * scale);
// Width of the resize grip around the card's visible border, in screen pixels.
// Shared with the InputNonClientPointerSource region registration in
// UpdateRegionsForCustomTitleBar so the band where WM_NCHITTEST fires lines up
// exactly with the band where we return resize codes.
var grip = (int)Math.Round(ResizeBorderThicknessDip * scale);
var onLeftEdge = ptX >= cardLeft - grip && ptX < cardLeft + grip;
var onRightEdge = ptX > cardRight - grip && ptX <= cardRight + grip;
var onTopEdge = ptY >= cardTop - grip && ptY < cardTop + grip;
var onBottomEdge = ptY > cardBottom - grip && ptY <= cardBottom + grip;
// Corners get priority over edges.
if (onTopEdge && onLeftEdge)
{
return PInvoke.HTTOPLEFT;
}
if (onTopEdge && onRightEdge)
{
return PInvoke.HTTOPRIGHT;
}
if (onBottomEdge && onLeftEdge)
{
return PInvoke.HTBOTTOMLEFT;
}
if (onBottomEdge && onRightEdge)
{
return PInvoke.HTBOTTOMRIGHT;
}
var withinHorizontalSpan = ptX >= cardLeft - grip && ptX <= cardRight + grip;
var withinVerticalSpan = ptY >= cardTop - grip && ptY <= cardBottom + grip;
if (onTopEdge && withinHorizontalSpan)
{
return PInvoke.HTTOP;
}
if (onBottomEdge && withinHorizontalSpan)
{
return PInvoke.HTBOTTOM;
}
if (onLeftEdge && withinVerticalSpan)
{
return PInvoke.HTLEFT;
}
if (onRightEdge && withinVerticalSpan)
{
return PInvoke.HTRIGHT;
}
// Pointer is inside the card but away from the border: defer to the default
// hit test so the InputNonClientPointerSource Caption/Passthrough regions take
// effect for dragging vs. normal input.
if (ptX >= cardLeft && ptX <= cardRight && ptY >= cardTop && ptY <= cardBottom)
{
return 0;
}
// Pointer is in the transparent shadow padding around the card. Make that area
// click-through so the window behind us receives the mouse input.
return unchecked((uint)PInvoke.HTTRANSPARENT);
}
public void Dispose()
{
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
App.Current.Services.GetRequiredService<ISettingsService>().SettingsChanged -= SettingsChangedHandler;
_localKeyboardListener.Dispose();
_windowThemeSynchronizer.Dispose();
DisposeAcrylic();
}
void IRecipient<ShowPaletteAtMessage>.Receive(ShowPaletteAtMessage message) => Receive(message);
public void Receive(ToggleDevRibbonMessage message)
{
_devRibbon?.Visibility = _devRibbon.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
}
public void Receive(DragStartedMessage message)
{
_preventHideWhenDeactivated = true;
}
public void Receive(DragCompletedMessage message)
{
_preventHideWhenDeactivated = false;
Task.Delay(200).ContinueWith(_ =>
{
DispatcherQueue.TryEnqueue(StealForeground);
});
}
private unsafe void StealForeground()
{
var foregroundWindow = PInvoke.GetForegroundWindow();
if (foregroundWindow == _hwnd)
{
return;
}
// This is bad, evil, and I'll have to forgo today's dinner dessert to punish myself
// for writing this. But there's no way to make this work without it.
// If the window is not reactivated, the UX breaks down: a deactivated window has to
// be activated and then deactivated again to hide.
var currentThreadId = PInvoke.GetCurrentThreadId();
var foregroundThreadId = PInvoke.GetWindowThreadProcessId(foregroundWindow, null);
if (foregroundThreadId != currentThreadId)
{
PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, true);
PInvoke.SetForegroundWindow(_hwnd);
PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, false);
}
else
{
PInvoke.SetForegroundWindow(_hwnd);
}
}
public void Receive(GetHwndMessage message)
{
message.Hwnd = this.GetWindowHandle();
}
public void Receive(ExpandCompactModeMessage message)
{
this.DispatcherQueue.TryEnqueue(() => HandleExpandCompactOnUiThread(message.Expanded));
}
public void Receive(MaximizeForDialogMessage message)
{
this.DispatcherQueue.TryEnqueue(() =>
{
_dialogFullExpandActive = message.Maximize;
// Re-run with the last requested state: when maximizing this fills the window; when
// the dialog closes it restores the normal compact/expanded layout.
HandleExpandCompactOnUiThread(_lastExpandRequested);
});
}
// The HWND is already as large as it will ever need to be (and it's transparent), so
// instead of resizing the window we simply shrink or grow the visible card inside it.
private void HandleExpandCompactOnUiThread(bool expanded)
{
_lastExpandRequested = expanded;
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
var preventCompactMode = _dialogFullExpandActive || !settings.CompactMode;
if (preventCompactMode)
{
// When compact mode is off, or a dialog is active, the card is
// always static and fills the entire window, regardless of how much
// content is currently displayed.
RootElement.SetCardStretch(true);
RootElement.SetCardMaxHeight(double.PositiveInfinity);
}
else
{
// In compact mode the card sizes itself to its content and anchors to the top.
RootElement.SetCardStretch(false);
// The HWND can extend below the current display after moving from a taller monitor.
// Always clamp an expanded compact card to the visible work area; the transparent
// portion of the HWND may remain off-screen, but the card and its content must not.
var cardMaxHeight = expanded
? ComputeExpandedCardMaxHeightDip()
: double.PositiveInfinity;
RootElement.SetCardMaxHeight(cardMaxHeight);
}
}
// Computes how tall (in DIPs) the visible card may grow before it would extend past the
// bottom of the work area, given the card's current top on screen.
private double ComputeExpandedCardMaxHeightDip()
{
var dpi = (int)this.GetDpiForWindow();
var scale = dpi / 96.0;
var padding = RootElement.ShadowPadding;
var cardTopPhysical = AppWindow.Position.Y + (padding.Top * scale);
// Select the display from the visible card rather than from the HWND. An oversized HWND
// can overlap another display (or have its center below the intended display), causing
// GetFromWindowId to choose a work area unrelated to the card the user is looking at.
var cardCenterXPhysical = AppWindow.Position.X + (AppWindow.Size.Width / 2);
var displayArea = DisplayArea.GetFromPoint(
new PointInt32(cardCenterXPhysical, (int)Math.Round(cardTopPhysical)),
DisplayAreaFallback.Nearest);
var workArea = displayArea.WorkArea;
var availablePhysical = (workArea.Y + workArea.Height) - cardTopPhysical - (padding.Bottom * scale);
return Math.Max(0, availablePhysical / scale);
}
}