mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-06-30 23:49:42 +02:00
Compare commits
12 Commits
main
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da880280be | ||
|
|
8d6a8e8a9e | ||
|
|
9f82e018f6 | ||
|
|
0c2bae21d4 | ||
|
|
c914bdf509 | ||
|
|
3851e06f45 | ||
|
|
b27ca5a9bf | ||
|
|
63701c9b91 | ||
|
|
cbb5e985c9 | ||
|
|
017e3278d9 | ||
|
|
e834031c7f | ||
|
|
981c1cefba |
@@ -365,6 +365,13 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
var oldWasEmpty = string.IsNullOrEmpty(oldSearch);
|
||||
var newWasEmpty = string.IsNullOrEmpty(newSearch);
|
||||
if (oldWasEmpty != newWasEmpty)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ExpandCompactModeMessage>(new(!newWasEmpty));
|
||||
}
|
||||
|
||||
UpdateSearchTextCore(oldSearch, newSearch, isUserInput: true);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// 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.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record ExpandCompactModeMessage(bool Expanded)
|
||||
{
|
||||
}
|
||||
@@ -42,6 +42,14 @@ public record SettingsModel
|
||||
|
||||
public bool AllowExternalReload { get; init; }
|
||||
|
||||
public bool CompactMode { get; set; } = true;
|
||||
|
||||
// When compact mode is on and the palette is centered on launch, this is the relative
|
||||
// height from the bottom of the screen (as a percentage) at which the collapsed search
|
||||
// box is vertically centered. 75 places it in the upper portion of the display. Ignored
|
||||
// when compact mode is off.
|
||||
public int CompactCenterHeightPercentage { get; set; } = 75;
|
||||
|
||||
private ImmutableDictionary<string, ProviderSettings>? _providerSettings
|
||||
= ImmutableDictionary<string, ProviderSettings>.Empty;
|
||||
|
||||
@@ -137,6 +145,18 @@ public record SettingsModel
|
||||
|
||||
// </Gallery settings>
|
||||
|
||||
// Internal diagnostics settings
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the main window's HWND chrome (title bar, border,
|
||||
/// system-drawn rounded corners) is visible. <strong>For internal debugging only.</strong>
|
||||
/// Off by default. The setting is persisted but only honored in non-CI builds; release /
|
||||
/// CI builds always force the borderless / transparent host window.
|
||||
/// </summary>
|
||||
public bool ShowHwndFrame { get; init; }
|
||||
|
||||
// </Internal diagnostics settings>
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -131,6 +131,25 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public bool CompactMode
|
||||
{
|
||||
get => _settingsService.Settings.CompactMode;
|
||||
set
|
||||
{
|
||||
_settingsService.UpdateSettings(s => s with { CompactMode = value });
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CompactMode)));
|
||||
}
|
||||
}
|
||||
|
||||
public double CompactCenterHeightPercentage
|
||||
{
|
||||
get => _settingsService.Settings.CompactCenterHeightPercentage;
|
||||
set
|
||||
{
|
||||
_settingsService.UpdateSettings(s => s with { CompactCenterHeightPercentage = (int)value });
|
||||
}
|
||||
}
|
||||
|
||||
public bool IgnoreShortcutWhenFullscreen
|
||||
{
|
||||
get => _settingsService.Settings.IgnoreShortcutWhenFullscreen;
|
||||
|
||||
@@ -85,6 +85,8 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
public bool IsNested => _isNested && !_currentlyTransient;
|
||||
|
||||
public bool IsTransient => _currentlyTransient;
|
||||
|
||||
public PageViewModel NullPage { get; private set; }
|
||||
|
||||
public ShellViewModel(
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.CmdPalMainControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Background="Transparent"
|
||||
IsTabStop="False"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ThemeShadow x:Key="CardShadow" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<!-- Outer transparent host. Padding leaves room for the drop shadow. -->
|
||||
<Grid x:Name="ShadowHost" Padding="{x:Bind ShadowPadding, Mode=OneWay}">
|
||||
|
||||
<!--
|
||||
The "card" — this is what looks like the cmdpal window.
|
||||
Border draws the 1px stroke and clips children to the rounded shape.
|
||||
ThemeShadow + a Translation on Z casts the drop shadow outside the card.
|
||||
-->
|
||||
<Border
|
||||
x:Name="CardBorder"
|
||||
VerticalAlignment="Top"
|
||||
Background="Transparent"
|
||||
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{x:Bind CardCornerRadius, Mode=OneWay}"
|
||||
Shadow="{StaticResource CardShadow}"
|
||||
Translation="0,0,32">
|
||||
|
||||
<Grid x:Name="CardContent">
|
||||
<!-- System backdrop (Mica / Acrylic / etc.) drawn only behind the card -->
|
||||
<controls:SystemBackdropElement x:Name="BackdropElement" CornerRadius="{x:Bind CardCornerRadius, Mode=OneWay}" />
|
||||
|
||||
<!-- Optional background image (sits between backdrop and content) -->
|
||||
<ContentPresenter
|
||||
x:Name="BackgroundLayerPresenter"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Content="{x:Bind BackgroundLayer, Mode=OneWay}"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<!-- Main UI content (e.g. ShellPage) -->
|
||||
<ContentPresenter
|
||||
x:Name="MainContentPresenter"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Content="{x:Bind MainContent, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,221 @@
|
||||
// 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 ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.UI;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// The visible "card" of the Command Palette — a control that renders the rounded
|
||||
/// corners, border, shadow and system backdrop. The HWND that hosts it is borderless
|
||||
/// and transparent, so all the chrome lives here instead of in window non-client area.
|
||||
/// </summary>
|
||||
public sealed partial class CmdPalMainControl : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty MainContentProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(MainContent),
|
||||
typeof(object),
|
||||
typeof(CmdPalMainControl),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public static readonly DependencyProperty BackgroundLayerProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(BackgroundLayer),
|
||||
typeof(object),
|
||||
typeof(CmdPalMainControl),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public static readonly DependencyProperty ShadowPaddingProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ShadowPadding),
|
||||
typeof(Thickness),
|
||||
typeof(CmdPalMainControl),
|
||||
new PropertyMetadata(new Thickness(16)));
|
||||
|
||||
public static readonly DependencyProperty CardCornerRadiusProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(CardCornerRadius),
|
||||
typeof(CornerRadius),
|
||||
typeof(CmdPalMainControl),
|
||||
new PropertyMetadata(new CornerRadius(8)));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the main UI content hosted inside the card (e.g. the ShellPage).
|
||||
/// </summary>
|
||||
public object? MainContent
|
||||
{
|
||||
get => GetValue(MainContentProperty);
|
||||
set => SetValue(MainContentProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a background layer rendered between the backdrop and the main content
|
||||
/// (e.g. the BlurImageControl). Hit-testing is disabled on this layer.
|
||||
/// </summary>
|
||||
public object? BackgroundLayer
|
||||
{
|
||||
get => GetValue(BackgroundLayerProperty);
|
||||
set => SetValue(BackgroundLayerProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the amount of transparent padding around the card. The drop shadow
|
||||
/// is rendered into this padded area.
|
||||
/// </summary>
|
||||
public Thickness ShadowPadding
|
||||
{
|
||||
get => (Thickness)GetValue(ShadowPaddingProperty);
|
||||
set => SetValue(ShadowPaddingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the corner radius of the card. Applied to both the clipping border
|
||||
/// and the backdrop element.
|
||||
/// </summary>
|
||||
public CornerRadius CardCornerRadius
|
||||
{
|
||||
get => (CornerRadius)GetValue(CardCornerRadiusProperty);
|
||||
set => SetValue(CardCornerRadiusProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible card border. Drag regions should be computed against this element
|
||||
/// so they line up with what the user sees, not the (larger, transparent) HWND.
|
||||
/// </summary>
|
||||
public FrameworkElement CardElement => CardBorder;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the panel inside the card that hosts the backdrop, background layer, and main
|
||||
/// content. Overlay UI (e.g. the dev ribbon) can be added to this panel so it draws
|
||||
/// inside the rounded card.
|
||||
/// </summary>
|
||||
public Panel CardContentPanel => CardContent;
|
||||
|
||||
public CmdPalMainControl()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the maximum height of the visible card (in DIPs). Use this to keep an expanded
|
||||
/// compact card from growing past the bottom of the display. Pass
|
||||
/// <see cref="double.PositiveInfinity"/> to remove the clamp.
|
||||
/// </summary>
|
||||
public void SetCardMaxHeight(double maxHeightDip)
|
||||
{
|
||||
CardBorder.MaxHeight = maxHeightDip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current height of the visible card (in DIPs). When the card is in its
|
||||
/// compact layout this is the height of just the search box, which callers use to center
|
||||
/// the collapsed card on screen.
|
||||
/// </summary>
|
||||
public double GetCardHeight()
|
||||
{
|
||||
CardBorder.UpdateLayout();
|
||||
return CardBorder.ActualHeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forwards the host window's activation state to the current backdrop so the system can
|
||||
/// render its active / inactive appearance correctly.
|
||||
/// </summary>
|
||||
public void SetIsInputActive(bool isActive)
|
||||
{
|
||||
if (BackdropElement.SystemBackdrop is TintedControllerBackdrop tinted)
|
||||
{
|
||||
tinted.IsInputActive = isActive;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches any backdrop from the embedded element. Used during shutdown to release the
|
||||
/// underlying controller eagerly.
|
||||
/// </summary>
|
||||
public void ClearBackdrop()
|
||||
{
|
||||
BackdropElement.SystemBackdrop = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a backdrop configuration to the embedded <see cref="SystemBackdropElement"/>.
|
||||
/// </summary>
|
||||
/// <param name="backdrop">Tint / opacity / fallback parameters from the theme service.</param>
|
||||
/// <param name="kind">The controller kind selected by the user's backdrop style.</param>
|
||||
/// <param name="isImageMode">When true, the background image control draws the tint, so no tint is applied to the backdrop itself.</param>
|
||||
/// <param name="hasColorization">When true, custom tint properties are applied to Mica backdrops.</param>
|
||||
public void ApplyBackdrop(BackdropParameters backdrop, BackdropControllerKind kind, bool isImageMode, bool hasColorization)
|
||||
{
|
||||
try
|
||||
{
|
||||
BackdropElement.SystemBackdrop = CreateBackdrop(backdrop, kind, isImageMode, hasColorization);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to apply backdrop to CmdPalMainControl", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Microsoft.UI.Xaml.Media.SystemBackdrop? CreateBackdrop(BackdropParameters backdrop, BackdropControllerKind kind, bool isImageMode, bool hasColorization)
|
||||
{
|
||||
// Image mode: don't tint here, BlurImageControl handles it (avoids double-tinting).
|
||||
var effectiveTintOpacity = isImageMode ? 0.0f : backdrop.EffectiveOpacity;
|
||||
|
||||
switch (kind)
|
||||
{
|
||||
case BackdropControllerKind.Solid:
|
||||
var solidTint = Color.FromArgb(
|
||||
(byte)(backdrop.EffectiveOpacity * 255),
|
||||
backdrop.TintColor.R,
|
||||
backdrop.TintColor.G,
|
||||
backdrop.TintColor.B);
|
||||
return new TransparentTintBackdrop { TintColor = solidTint };
|
||||
|
||||
case BackdropControllerKind.Mica:
|
||||
case BackdropControllerKind.MicaAlt:
|
||||
if (!MicaController.IsSupported())
|
||||
{
|
||||
return new TransparentTintBackdrop { TintColor = backdrop.FallbackColor };
|
||||
}
|
||||
|
||||
return new TintedMicaBackdrop
|
||||
{
|
||||
Kind = kind == BackdropControllerKind.MicaAlt ? MicaKind.BaseAlt : MicaKind.Base,
|
||||
ApplyTint = hasColorization || isImageMode,
|
||||
TintColor = backdrop.TintColor,
|
||||
TintOpacity = effectiveTintOpacity,
|
||||
FallbackColor = backdrop.FallbackColor,
|
||||
LuminosityOpacity = backdrop.EffectiveLuminosityOpacity,
|
||||
};
|
||||
|
||||
case BackdropControllerKind.Acrylic:
|
||||
case BackdropControllerKind.AcrylicThin:
|
||||
default:
|
||||
if (!DesktopAcrylicController.IsSupported())
|
||||
{
|
||||
return new TransparentTintBackdrop { TintColor = backdrop.FallbackColor };
|
||||
}
|
||||
|
||||
return new TintedDesktopAcrylicBackdrop
|
||||
{
|
||||
Kind = kind == BackdropControllerKind.AcrylicThin
|
||||
? DesktopAcrylicKind.Thin
|
||||
: DesktopAcrylicKind.Default,
|
||||
TintColor = backdrop.TintColor,
|
||||
TintOpacity = effectiveTintOpacity,
|
||||
FallbackColor = backdrop.FallbackColor,
|
||||
LuminosityOpacity = backdrop.EffectiveLuminosityOpacity,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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 Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for tinted backdrops that wrap a controller from
|
||||
/// <see cref="Microsoft.UI.Composition.SystemBackdrops"/> so they can be applied
|
||||
/// to a single control via <see cref="Microsoft.UI.Xaml.Controls.SystemBackdropElement"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The stock <see cref="MicaBackdrop"/> / <see cref="DesktopAcrylicBackdrop"/> classes
|
||||
/// don't expose tint color / opacity / luminosity customization. This base type plugs
|
||||
/// the lower-level controllers into the new <see cref="Microsoft.UI.Xaml.Controls.SystemBackdropElement"/>
|
||||
/// extensibility surface so we can keep all of CmdPal's theme-driven tinting.
|
||||
/// </remarks>
|
||||
internal abstract partial class TintedControllerBackdrop : SystemBackdrop
|
||||
{
|
||||
private SystemBackdropConfiguration? _config;
|
||||
|
||||
public Color TintColor { get; init; }
|
||||
|
||||
public float TintOpacity { get; init; }
|
||||
|
||||
public Color FallbackColor { get; init; }
|
||||
|
||||
public float LuminosityOpacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether tint properties should be applied. Mica without
|
||||
/// colorization wants the system defaults; in that case set this to false.
|
||||
/// </summary>
|
||||
public bool ApplyTint { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the host window is currently activated. The
|
||||
/// system uses this to decide between the active and inactive backdrop appearance.
|
||||
/// </summary>
|
||||
public bool IsInputActive
|
||||
{
|
||||
get => _config?.IsInputActive ?? true;
|
||||
set
|
||||
{
|
||||
if (_config is not null)
|
||||
{
|
||||
_config.IsInputActive = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected SystemBackdropConfiguration? Configuration => _config;
|
||||
|
||||
protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot)
|
||||
{
|
||||
base.OnTargetConnected(connectedTarget, xamlRoot);
|
||||
_config = new SystemBackdropConfiguration
|
||||
{
|
||||
IsInputActive = true,
|
||||
Theme = xamlRoot.Content is FrameworkElement fe
|
||||
? ToBackdropTheme(fe.ActualTheme)
|
||||
: SystemBackdropTheme.Default,
|
||||
};
|
||||
AttachController(connectedTarget, xamlRoot);
|
||||
}
|
||||
|
||||
protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget)
|
||||
{
|
||||
DetachController(disconnectedTarget);
|
||||
_config = null;
|
||||
base.OnTargetDisconnected(disconnectedTarget);
|
||||
}
|
||||
|
||||
protected abstract void AttachController(ICompositionSupportsSystemBackdrop target, XamlRoot xamlRoot);
|
||||
|
||||
protected abstract void DetachController(ICompositionSupportsSystemBackdrop target);
|
||||
|
||||
private static SystemBackdropTheme ToBackdropTheme(ElementTheme theme) => theme switch
|
||||
{
|
||||
ElementTheme.Dark => SystemBackdropTheme.Dark,
|
||||
ElementTheme.Light => SystemBackdropTheme.Light,
|
||||
_ => SystemBackdropTheme.Default,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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 Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A tinted <see cref="DesktopAcrylicController"/> exposed as a <see cref="SystemBackdrop"/>
|
||||
/// so it can be hosted by <see cref="Microsoft.UI.Xaml.Controls.SystemBackdropElement"/>.
|
||||
/// </summary>
|
||||
internal sealed partial class TintedDesktopAcrylicBackdrop : TintedControllerBackdrop, IDisposable
|
||||
{
|
||||
private DesktopAcrylicController? _controller;
|
||||
|
||||
public DesktopAcrylicKind Kind { get; init; } = DesktopAcrylicKind.Default;
|
||||
|
||||
protected override void AttachController(ICompositionSupportsSystemBackdrop target, XamlRoot xamlRoot)
|
||||
{
|
||||
if (!DesktopAcrylicController.IsSupported())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controller = new DesktopAcrylicController
|
||||
{
|
||||
Kind = Kind,
|
||||
TintColor = TintColor,
|
||||
TintOpacity = TintOpacity,
|
||||
FallbackColor = FallbackColor,
|
||||
LuminosityOpacity = LuminosityOpacity,
|
||||
};
|
||||
|
||||
_controller.AddSystemBackdropTarget(target);
|
||||
_controller.SetSystemBackdropConfiguration(Configuration);
|
||||
}
|
||||
|
||||
protected override void DetachController(ICompositionSupportsSystemBackdrop target)
|
||||
{
|
||||
if (_controller is not null)
|
||||
{
|
||||
_controller.RemoveSystemBackdropTarget(target);
|
||||
_controller.Dispose();
|
||||
_controller = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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 Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A tinted <see cref="MicaController"/> exposed as a <see cref="SystemBackdrop"/>
|
||||
/// so it can be hosted by <see cref="Microsoft.UI.Xaml.Controls.SystemBackdropElement"/>.
|
||||
/// </summary>
|
||||
internal sealed partial class TintedMicaBackdrop : TintedControllerBackdrop, IDisposable
|
||||
{
|
||||
private MicaController? _controller;
|
||||
|
||||
public MicaKind Kind { get; init; } = MicaKind.Base;
|
||||
|
||||
protected override void AttachController(ICompositionSupportsSystemBackdrop target, XamlRoot xamlRoot)
|
||||
{
|
||||
if (!MicaController.IsSupported())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controller = new MicaController { Kind = Kind };
|
||||
|
||||
// Only set tint properties when colorization is active.
|
||||
// Otherwise let the system handle light/dark theme defaults automatically.
|
||||
if (ApplyTint)
|
||||
{
|
||||
_controller.TintColor = TintColor;
|
||||
_controller.TintOpacity = TintOpacity;
|
||||
_controller.FallbackColor = FallbackColor;
|
||||
_controller.LuminosityOpacity = LuminosityOpacity;
|
||||
}
|
||||
|
||||
_controller.AddSystemBackdropTarget(target);
|
||||
_controller.SetSystemBackdropConfiguration(Configuration);
|
||||
}
|
||||
|
||||
protected override void DetachController(ICompositionSupportsSystemBackdrop target)
|
||||
{
|
||||
if (_controller is not null)
|
||||
{
|
||||
_controller.RemoveSystemBackdropTarget(target);
|
||||
_controller.Dispose();
|
||||
_controller = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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 Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a boolean to a <see cref="GridLength"/>: <c>true</c> yields a star (*) row that
|
||||
/// fills the available space, while <c>false</c> yields an Auto row that sizes to its content.
|
||||
/// This lets the expandable content row collapse to zero in compact mode so the card can
|
||||
/// shrink to just the search box (a star row would otherwise reserve space during measure
|
||||
/// even when its only child is collapsed).
|
||||
/// </summary>
|
||||
public partial class BoolToStarOrAutoGridLengthConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
var expanded = value is bool b && b;
|
||||
return expanded ? new GridLength(1, GridUnitType.Star) : GridLength.Auto;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
|
||||
}
|
||||
@@ -15,22 +15,32 @@
|
||||
Activated="MainWindow_Activated"
|
||||
Closed="MainWindow_Closed"
|
||||
mc:Ignorable="d">
|
||||
<Grid x:Name="RootElement">
|
||||
|
||||
<controls:BlurImageControl
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BlurAmount="{x:Bind ViewModel.BackgroundImageBlurAmount, Mode=OneWay}"
|
||||
ImageBrightness="{x:Bind ViewModel.BackgroundImageBrightness, Mode=OneWay}"
|
||||
ImageOpacity="{x:Bind ViewModel.EffectiveImageOpacity, Mode=OneWay}"
|
||||
ImageSource="{x:Bind ViewModel.BackgroundImageSource, Mode=OneWay}"
|
||||
ImageStretch="{x:Bind ViewModel.BackgroundImageStretch, Mode=OneWay}"
|
||||
IsHitTestVisible="False"
|
||||
IsHoldingEnabled="False"
|
||||
TintColor="{x:Bind ViewModel.BackgroundImageTint, Mode=OneWay}"
|
||||
TintIntensity="{x:Bind ViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.ShowBackgroundImage, Mode=OneWay}" />
|
||||
|
||||
<pages:ShellPage HostWindow="{x:Bind}" />
|
||||
</Grid>
|
||||
<!--
|
||||
The whole window is borderless and transparent (see MainWindow.xaml.cs).
|
||||
CmdPalMainControl is the visible "card" — it draws the rounded corners,
|
||||
border, drop shadow, and hosts the SystemBackdropElement that paints
|
||||
Mica / Acrylic / etc. behind the content.
|
||||
-->
|
||||
<controls:CmdPalMainControl x:Name="RootElement">
|
||||
<controls:CmdPalMainControl.BackgroundLayer>
|
||||
<controls:BlurImageControl
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BlurAmount="{x:Bind ViewModel.BackgroundImageBlurAmount, Mode=OneWay}"
|
||||
ImageBrightness="{x:Bind ViewModel.BackgroundImageBrightness, Mode=OneWay}"
|
||||
ImageOpacity="{x:Bind ViewModel.EffectiveImageOpacity, Mode=OneWay}"
|
||||
ImageSource="{x:Bind ViewModel.BackgroundImageSource, Mode=OneWay}"
|
||||
ImageStretch="{x:Bind ViewModel.BackgroundImageStretch, Mode=OneWay}"
|
||||
IsHitTestVisible="False"
|
||||
IsHoldingEnabled="False"
|
||||
TintColor="{x:Bind ViewModel.BackgroundImageTint, Mode=OneWay}"
|
||||
TintIntensity="{x:Bind ViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.ShowBackgroundImage, Mode=OneWay}" />
|
||||
</controls:CmdPalMainControl.BackgroundLayer>
|
||||
<controls:CmdPalMainControl.MainContent>
|
||||
<pages:ShellPage HostWindow="{x:Bind}" />
|
||||
</controls:CmdPalMainControl.MainContent>
|
||||
</controls:CmdPalMainControl>
|
||||
</winuiex:WindowEx>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ 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;
|
||||
@@ -22,8 +23,7 @@ using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.ViewModels.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -32,13 +32,11 @@ using Windows.ApplicationModel.Activation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics;
|
||||
using Windows.System;
|
||||
using Windows.UI;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using WinRT;
|
||||
using WinUIEx;
|
||||
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
|
||||
|
||||
@@ -58,6 +56,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<DragCompletedMessage>,
|
||||
IRecipient<ToggleDevRibbonMessage>,
|
||||
IRecipient<GetHwndMessage>,
|
||||
IRecipient<ExpandCompactModeMessage>,
|
||||
IDisposable,
|
||||
IHostWindow
|
||||
{
|
||||
@@ -90,12 +89,19 @@ public sealed partial class MainWindow : WindowEx,
|
||||
private int _sessionMaxNavigationDepth;
|
||||
private int _sessionErrorCount;
|
||||
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
private MicaController? _micaController;
|
||||
private SystemBackdropConfiguration? _configurationSource;
|
||||
private bool _isUpdatingBackdrop;
|
||||
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;
|
||||
@@ -127,7 +133,13 @@ public sealed partial class MainWindow : WindowEx,
|
||||
CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value);
|
||||
}
|
||||
|
||||
InitializeBackdropSupport();
|
||||
// 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);
|
||||
|
||||
@@ -164,6 +176,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ToggleDevRibbonMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<GetHwndMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ExpandCompactModeMessage>(this);
|
||||
|
||||
// Hide our titlebar.
|
||||
// We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed
|
||||
@@ -232,16 +245,24 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// the DIP size won't trigger it, leaving drag regions at the old physical coordinates.
|
||||
RootElement.XamlRoot.Changed += XamlRoot_Changed;
|
||||
|
||||
// Add dev ribbon if enabled
|
||||
// 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.Children.Add(_devRibbon);
|
||||
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()
|
||||
@@ -275,10 +296,77 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
if (rect is not null)
|
||||
{
|
||||
MoveAndResizeDpiAware(rect.Value);
|
||||
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)
|
||||
@@ -380,17 +468,104 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
_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>
|
||||
/// 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));
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeBackdropSupport()
|
||||
{
|
||||
if (DesktopAcrylicController.IsSupported() || MicaController.IsSupported())
|
||||
{
|
||||
_configurationSource = new SystemBackdropConfiguration
|
||||
{
|
||||
IsInputActive = true,
|
||||
};
|
||||
}
|
||||
// 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()
|
||||
@@ -403,35 +578,14 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
_isUpdatingBackdrop = true;
|
||||
|
||||
var backdrop = _themeService.Current.BackdropParameters;
|
||||
var isImageMode = ViewModel.ShowBackgroundImage;
|
||||
var config = BackdropStyles.Get(backdrop.Style);
|
||||
|
||||
try
|
||||
{
|
||||
switch (config.ControllerKind)
|
||||
{
|
||||
case BackdropControllerKind.Solid:
|
||||
CleanupBackdropControllers();
|
||||
var tintColor = Color.FromArgb(
|
||||
(byte)(backdrop.EffectiveOpacity * 255),
|
||||
backdrop.TintColor.R,
|
||||
backdrop.TintColor.G,
|
||||
backdrop.TintColor.B);
|
||||
SetupTransparentBackdrop(tintColor);
|
||||
break;
|
||||
var backdrop = _themeService.Current.BackdropParameters;
|
||||
var isImageMode = ViewModel.ShowBackgroundImage;
|
||||
var config = BackdropStyles.Get(backdrop.Style);
|
||||
var hasColorization = _themeService.Current.HasColorization;
|
||||
|
||||
case BackdropControllerKind.Mica:
|
||||
case BackdropControllerKind.MicaAlt:
|
||||
SetupMica(backdrop, isImageMode, config.ControllerKind);
|
||||
break;
|
||||
|
||||
case BackdropControllerKind.Acrylic:
|
||||
case BackdropControllerKind.AcrylicThin:
|
||||
default:
|
||||
SetupDesktopAcrylic(backdrop, isImageMode, config.ControllerKind);
|
||||
break;
|
||||
}
|
||||
RootElement.ApplyBackdrop(backdrop, config.ControllerKind, isImageMode, hasColorization);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -443,111 +597,6 @@ public sealed partial class MainWindow : WindowEx,
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTransparentBackdrop(Color tintColor)
|
||||
{
|
||||
if (SystemBackdrop is TransparentTintBackdrop existingBackdrop)
|
||||
{
|
||||
existingBackdrop.TintColor = tintColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
SystemBackdrop = new TransparentTintBackdrop { TintColor = tintColor };
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupBackdropControllers()
|
||||
{
|
||||
if (_acrylicController is not null)
|
||||
{
|
||||
_acrylicController.RemoveAllSystemBackdropTargets();
|
||||
_acrylicController.Dispose();
|
||||
_acrylicController = null;
|
||||
}
|
||||
|
||||
if (_micaController is not null)
|
||||
{
|
||||
_micaController.RemoveAllSystemBackdropTargets();
|
||||
_micaController.Dispose();
|
||||
_micaController = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupDesktopAcrylic(BackdropParameters backdrop, bool isImageMode, BackdropControllerKind kind)
|
||||
{
|
||||
CleanupBackdropControllers();
|
||||
|
||||
// Fall back to solid color if acrylic not supported
|
||||
if (_configurationSource is null || !DesktopAcrylicController.IsSupported())
|
||||
{
|
||||
SetupTransparentBackdrop(backdrop.FallbackColor);
|
||||
return;
|
||||
}
|
||||
|
||||
// DesktopAcrylicController and SystemBackdrop can't be active simultaneously
|
||||
SystemBackdrop = null;
|
||||
|
||||
// Image mode: no tint here, BlurImageControl handles it (avoids double-tinting)
|
||||
var effectiveTintOpacity = isImageMode
|
||||
? 0.0f
|
||||
: backdrop.EffectiveOpacity;
|
||||
|
||||
_acrylicController = new DesktopAcrylicController
|
||||
{
|
||||
Kind = kind == BackdropControllerKind.AcrylicThin
|
||||
? DesktopAcrylicKind.Thin
|
||||
: DesktopAcrylicKind.Default,
|
||||
TintColor = backdrop.TintColor,
|
||||
TintOpacity = effectiveTintOpacity,
|
||||
FallbackColor = backdrop.FallbackColor,
|
||||
LuminosityOpacity = backdrop.EffectiveLuminosityOpacity,
|
||||
};
|
||||
|
||||
// Requires "using WinRT;" for Window.As<>()
|
||||
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
||||
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
|
||||
}
|
||||
|
||||
private void SetupMica(BackdropParameters backdrop, bool isImageMode, BackdropControllerKind kind)
|
||||
{
|
||||
CleanupBackdropControllers();
|
||||
|
||||
// Fall back to solid color if Mica not supported
|
||||
if (_configurationSource is null || !MicaController.IsSupported())
|
||||
{
|
||||
SetupTransparentBackdrop(backdrop.FallbackColor);
|
||||
return;
|
||||
}
|
||||
|
||||
// MicaController and SystemBackdrop can't be active simultaneously
|
||||
SystemBackdrop = null;
|
||||
_configurationSource.Theme = _themeService.Current.Theme == ElementTheme.Dark
|
||||
? SystemBackdropTheme.Dark
|
||||
: SystemBackdropTheme.Light;
|
||||
|
||||
var hasColorization = _themeService.Current.HasColorization || isImageMode;
|
||||
|
||||
_micaController = new MicaController
|
||||
{
|
||||
Kind = kind == BackdropControllerKind.MicaAlt
|
||||
? MicaKind.BaseAlt
|
||||
: MicaKind.Base,
|
||||
};
|
||||
|
||||
// Only set tint properties when colorization is active
|
||||
// Otherwise let system handle light/dark theme defaults automatically
|
||||
if (hasColorization)
|
||||
{
|
||||
// Image mode: no tint here, BlurImageControl handles it (avoids double-tinting)
|
||||
_micaController.TintColor = backdrop.TintColor;
|
||||
_micaController.TintOpacity = isImageMode ? 0.0f : backdrop.EffectiveOpacity;
|
||||
_micaController.FallbackColor = backdrop.FallbackColor;
|
||||
_micaController.LuminosityOpacity = backdrop.EffectiveLuminosityOpacity;
|
||||
}
|
||||
|
||||
_micaController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
||||
_micaController.SetSystemBackdropConfiguration(_configurationSource);
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
|
||||
{
|
||||
var positionWindowForTargetMonitor = (HWND hwnd) =>
|
||||
@@ -654,6 +703,13 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// 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 process start 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. Re-applying here makes the borderless look stick on cold start.
|
||||
ApplyHwndBorderAttributes(_hwndFrameVisible ?? false);
|
||||
|
||||
// Once we're done, uncloak to avoid all animations
|
||||
Uncloak();
|
||||
|
||||
@@ -939,8 +995,17 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void DisposeAcrylic()
|
||||
{
|
||||
CleanupBackdropControllers();
|
||||
_configurationSource = null!;
|
||||
// The backdrop controllers now live on the SystemBackdropElement inside
|
||||
// CmdPalMainControl. Clearing its SystemBackdrop fires OnTargetDisconnected on the
|
||||
// current backdrop, which removes targets and disposes the underlying controller.
|
||||
try
|
||||
{
|
||||
RootElement?.ClearBackdrop();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort cleanup; ignore errors during shutdown.
|
||||
}
|
||||
}
|
||||
|
||||
// Updates our window s.t. the top of the window is draggable.
|
||||
@@ -955,31 +1020,113 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// Specify the interactive regions of the title bar.
|
||||
var scaleAdjustment = xamlRoot.RasterizationScale;
|
||||
|
||||
// Get the rectangle around our XAML content. We're going to mark this
|
||||
// rectangle as "Passthrough", so that the normal window operations
|
||||
// (resizing, dragging) don't apply in this space.
|
||||
var transform = RootElement.TransformToVisual(null);
|
||||
|
||||
// Reserve 16px of space at the top for dragging.
|
||||
var topHeight = 16;
|
||||
var bounds = transform.TransformBounds(new Rect(
|
||||
0,
|
||||
topHeight,
|
||||
RootElement.ActualWidth,
|
||||
RootElement.ActualHeight));
|
||||
var contentRect = GetRect(bounds, scaleAdjustment);
|
||||
var rectArray = new RectInt32[] { contentRect };
|
||||
var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(this.AppWindow.Id);
|
||||
nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rectArray);
|
||||
|
||||
// Add a drag-able region on top
|
||||
var w = RootElement.ActualWidth;
|
||||
_ = RootElement.ActualHeight;
|
||||
var dragSides = new RectInt32[]
|
||||
// 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)
|
||||
{
|
||||
GetRect(new Rect(0, 0, w, topHeight), scaleAdjustment), // the top, {topHeight=16} tall
|
||||
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 AND the top drag bar as non-client (Caption).
|
||||
// This is the critical bit: only regions registered here generate WM_NCHITTEST
|
||||
// on our window proc. Without the side/bottom strips, the XAML content island
|
||||
// swallows the pointer and we never get a hit-test to turn into a resize. Our
|
||||
// HotKeyPrc WM_NCHITTEST handler then decides drag (caption) vs. resize
|
||||
// (HTLEFT / HTRIGHT / HT{TOP,BOTTOM}{,LEFT,RIGHT}) per-pixel via geometry.
|
||||
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, dragSides);
|
||||
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 itself down to just the visible card. The window is intentionally
|
||||
// larger than the card (the extra margin holds the drop shadow), but that
|
||||
// transparent margin shouldn't be part of the window at all: clicks there should
|
||||
// fall through to whatever is behind the palette, and the shadow must not be
|
||||
// hit-testable. The region is updated here so it tracks the card as it grows /
|
||||
// shrinks (e.g. compact <-> expanded).
|
||||
ApplyCardWindowRegion(CardRect(0, 0, w, h));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restricts the HWND's visible / hit-testable area to the rectangle occupied by the
|
||||
/// visible card (supplied in physical client pixels). Everything outside — the
|
||||
/// transparent drop-shadow margin — 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 cardPhysical)
|
||||
{
|
||||
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
|
||||
// card's client-space physical rect maps directly into window space.
|
||||
var region = CreateRectRgn(
|
||||
cardPhysical.X,
|
||||
cardPhysical.Y,
|
||||
cardPhysical.X + cardPhysical.Width,
|
||||
cardPhysical.Y + cardPhysical.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)
|
||||
@@ -991,6 +1138,19 @@ public sealed partial class MainWindow : WindowEx,
|
||||
_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);
|
||||
|
||||
internal void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (!_themeServiceInitialized && args.WindowActivationState != WindowActivationState.Deactivated)
|
||||
@@ -1043,9 +1203,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
|
||||
}
|
||||
|
||||
if (_configurationSource is not null)
|
||||
if (RootElement is not null)
|
||||
{
|
||||
_configurationSource.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated;
|
||||
RootElement.SetIsInputActive(args.WindowActivationState != WindowActivationState.Deactivated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1333,6 +1493,29 @@ public sealed partial class MainWindow : WindowEx,
|
||||
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;
|
||||
|
||||
case PInvoke.WM_HOTKEY:
|
||||
{
|
||||
var hotkeyIndex = (int)wParam.Value;
|
||||
@@ -1357,6 +1540,116 @@ public sealed partial class MainWindow : WindowEx,
|
||||
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()
|
||||
{
|
||||
_localKeyboardListener.Dispose();
|
||||
@@ -1415,4 +1708,51 @@ public sealed partial class MainWindow : WindowEx,
|
||||
{
|
||||
message.Hwnd = this.GetWindowHandle();
|
||||
}
|
||||
|
||||
public void Receive(ExpandCompactModeMessage message)
|
||||
{
|
||||
this.DispatcherQueue.TryEnqueue(() => HandleExpandCompactOnUiThread(message.Expanded));
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
|
||||
|
||||
// Only the compact + centered configuration needs a screen-fit clamp. There the card
|
||||
// is anchored near the vertical center of the display, so an expanded list could run
|
||||
// off the bottom edge; cap its height so it always fits. In every other case the card
|
||||
// is free to fill the (fixed-size) HWND as before.
|
||||
if (expanded && settings.CompactMode && IsCenteringSummon(settings))
|
||||
{
|
||||
RootElement.SetCardMaxHeight(ComputeExpandedCardMaxHeightDip());
|
||||
}
|
||||
else
|
||||
{
|
||||
RootElement.SetCardMaxHeight(double.PositiveInfinity);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest);
|
||||
var workArea = displayArea.WorkArea;
|
||||
|
||||
var padding = RootElement.ShadowPadding;
|
||||
var cardTopPhysical = AppWindow.Position.Y + (padding.Top * scale);
|
||||
var availablePhysical = (workArea.Y + workArea.Height) - cardTopPhysical - (padding.Bottom * scale);
|
||||
|
||||
if (availablePhysical <= 0)
|
||||
{
|
||||
return double.PositiveInfinity;
|
||||
}
|
||||
|
||||
return availablePhysical / scale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,19 @@ SIZE_MINIMIZED
|
||||
HWND_NOTOPMOST
|
||||
HWND_TOP
|
||||
HTCAPTION
|
||||
HTCLIENT
|
||||
HTTRANSPARENT
|
||||
HTNOWHERE
|
||||
HTLEFT
|
||||
HTRIGHT
|
||||
HTTOP
|
||||
HTBOTTOM
|
||||
HTTOPLEFT
|
||||
HTTOPRIGHT
|
||||
HTBOTTOMLEFT
|
||||
HTBOTTOMRIGHT
|
||||
WM_NCHITTEST
|
||||
WM_NCCALCSIZE
|
||||
GetClassName
|
||||
EVENT_SYSTEM_FOREGROUND
|
||||
WINEVENT_OUTOFCONTEXT
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<cmdpalUI:DetailsSizeToGridLengthConverter x:Key="SizeToWidthConverter" />
|
||||
<cmdpalUI:MessageStateToSeverityConverter x:Key="MessageStateToSeverityConverter" />
|
||||
<cmdpalUI:BoolToStarOrAutoGridLengthConverter x:Key="ExpandedModeToRowHeightConverter" />
|
||||
|
||||
<cmdpalUI:DetailsDataTemplateSelector
|
||||
x:Key="DetailsDataTemplateSelector"
|
||||
@@ -183,15 +184,19 @@
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<!--
|
||||
In compact mode this row collapses to Auto so the card can shrink to just the
|
||||
search box. A star row would otherwise reserve space during measure even when
|
||||
its only child (the collapsed content) is hidden.
|
||||
-->
|
||||
<RowDefinition Height="{x:Bind ExpandedMode, Mode=OneWay, Converter={StaticResource ExpandedModeToRowHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Background="{ThemeResource LayerOnAcrylicPrimaryBackgroundBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Back button and search box -->
|
||||
@@ -383,6 +388,18 @@
|
||||
</animations:Implicit.HideAnimations>
|
||||
</ProgressBar>
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Background="{ThemeResource LayerOnAcrylicPrimaryBackgroundBrush}"
|
||||
Visibility="{x:Bind ExpandedMode, Mode=OneWay}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid x:Name="ContentGrid" Grid.Row="1">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -516,12 +533,13 @@
|
||||
See https://github.com/microsoft/microsoft-ui-xaml/issues/5741
|
||||
-->
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Row="1"
|
||||
Margin="16,8,16,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}">
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||
Visibility="Collapsed">
|
||||
<InfoBar
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}"
|
||||
IsOpen="{x:Bind ViewModel.CurrentPage.HasStatusMessage, Mode=OneWay}"
|
||||
@@ -540,10 +558,11 @@
|
||||
</StackPanel>
|
||||
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.Row="2"
|
||||
Background="{ThemeResource LayerOnAcrylicSecondaryBackgroundBrush}"
|
||||
BorderBrush="{ThemeResource CmdPal.DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0">
|
||||
BorderThickness="0,1,0,0"
|
||||
Visibility="{x:Bind ExpandedMode, Mode=OneWay}">
|
||||
<cpcontrols:CommandBar CurrentPageViewModel="{x:Bind ViewModel.CurrentPage, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
IRecipient<NavigateToPageMessage>,
|
||||
IRecipient<ShowHideDockMessage>,
|
||||
IRecipient<ShowPinToDockDialogMessage>,
|
||||
IRecipient<ExpandCompactModeMessage>,
|
||||
INotifyPropertyChanged,
|
||||
IDisposable
|
||||
{
|
||||
@@ -71,6 +72,13 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
private CancellationTokenSource? _focusAfterLoadedCts;
|
||||
private WeakReference<Page>? _lastNavigatedPageRef;
|
||||
|
||||
// When the shell goes from compact (collapsed) to expanded, the content frame's page
|
||||
// — which was collapsed and therefore never laid out — finally fires its Loaded event.
|
||||
// That late Loaded would otherwise run the post-navigation focus/select logic and
|
||||
// select-all the character the user just typed (which triggered the expand). This
|
||||
// one-shot flag suppresses that select for the expand-driven load.
|
||||
private bool _suppressSelectOnNextLoad;
|
||||
private bool _isDisposed;
|
||||
|
||||
public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService<ShellViewModel>()!;
|
||||
@@ -79,8 +87,13 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
public IHostWindow? HostWindow { get; set; }
|
||||
|
||||
public bool ExpandedMode { get; set; }
|
||||
|
||||
public ShellPage()
|
||||
{
|
||||
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
|
||||
this.ExpandedMode = !settings.CompactMode;
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
// how we are doing navigation around
|
||||
@@ -104,6 +117,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
WeakReferenceMessenger.Default.Register<ShowHideDockMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowPinToDockDialogMessage>(this);
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ExpandCompactModeMessage>(this);
|
||||
|
||||
AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true);
|
||||
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
|
||||
AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
|
||||
@@ -470,6 +485,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
// When re-showing the palette, the previous session's query may still be present
|
||||
// (e.g. after a light dismiss with HighlightSearchOnActivate). Recompute the
|
||||
// compact/expanded state so a retained query restores the expanded results instead
|
||||
// of being stuck in the collapsed search-only layout.
|
||||
UpdateCompactModeForCurrentPage();
|
||||
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
|
||||
@@ -581,6 +602,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
|
||||
{
|
||||
// A real navigation always loads a fresh page that we do want to focus/select, so
|
||||
// clear any stale suppression left over from a prior compact expand. (If this
|
||||
// navigation itself expands compact mode, UpdateCompactModeForCurrentPage below
|
||||
// will re-arm the flag for the page that's about to load.)
|
||||
_suppressSelectOnNextLoad = false;
|
||||
|
||||
// This listens to the root frame to ensure that we also track the content's page VM as well that we passed as a parameter.
|
||||
// This is currently used for both forward and backward navigation.
|
||||
// As when we go back that we restore ourselves to the proper state within our VM
|
||||
@@ -617,6 +644,42 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
_lastNavigatedPageRef = new WeakReference<Page>(element);
|
||||
element.Loaded += FocusAfterLoaded;
|
||||
}
|
||||
|
||||
UpdateCompactModeForCurrentPage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the compact/expanded state after a navigation. On any nested (sub) page we
|
||||
/// always show the full expanded UI; on the root page the search box drives the state,
|
||||
/// so we collapse to the compact search box only when the query is empty. Driving this
|
||||
/// from navigation (rather than only from search-text changes) makes alias-based
|
||||
/// navigation expand correctly — an alias clears the search box before navigating, so
|
||||
/// the search-text transition alone would otherwise leave the palette collapsed.
|
||||
/// Transient pages always show the expanded UI, ignoring the compact setting entirely.
|
||||
/// </summary>
|
||||
private void UpdateCompactModeForCurrentPage()
|
||||
{
|
||||
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
|
||||
if (!settings.CompactMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Transient pages ignore compact mode and always present as expanded.
|
||||
if (ViewModel.IsTransient)
|
||||
{
|
||||
HandleExpandCompactOnUiThread(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// The ShellViewModel's IsNested flag is only updated on forward navigation and is
|
||||
// never cleared when navigating back to the root page. Gate it on the current
|
||||
// page's own root-ness so a stale IsNested can't keep the home page expanded after
|
||||
// returning to it (e.g. after following a 1-character alias and going back).
|
||||
var isRootPage = ViewModel.CurrentPage?.IsRootPage ?? false;
|
||||
var nested = ViewModel.IsNested && !isRootPage;
|
||||
var hasQuery = !string.IsNullOrEmpty(ViewModel.CurrentPage?.SearchTextBox);
|
||||
HandleExpandCompactOnUiThread(nested || hasQuery);
|
||||
}
|
||||
|
||||
private void FocusAfterLoaded(object sender, RoutedEventArgs e)
|
||||
@@ -649,6 +712,15 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
return;
|
||||
}
|
||||
|
||||
// This Loaded can fire late when expanding out of compact mode (the page was
|
||||
// collapsed and never laid out). In that case the user is mid-typing in the
|
||||
// already-focused search box, so don't steal focus / select-all their input.
|
||||
if (_suppressSelectOnNextLoad)
|
||||
{
|
||||
_suppressSelectOnNextLoad = false;
|
||||
return;
|
||||
}
|
||||
|
||||
SearchBox.Focus(FocusState.Programmatic);
|
||||
SearchBox.SelectSearch();
|
||||
}
|
||||
@@ -842,6 +914,50 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ExpandCompactModeMessage message)
|
||||
{
|
||||
// Re-evaluate from the current authoritative page state rather than applying the
|
||||
// message's snapshot directly. The message can race with navigation: following a
|
||||
// 1-character alias clears the home search (sending a "collapse") right as we
|
||||
// navigate to a nested page that must stay expanded. Recomputing here keeps the
|
||||
// final state consistent regardless of message/navigation ordering.
|
||||
this.DispatcherQueue.TryEnqueue(UpdateCompactModeForCurrentPage);
|
||||
}
|
||||
|
||||
private void HandleExpandCompactOnUiThread(bool expanded)
|
||||
{
|
||||
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
|
||||
var newExpanded = settings.CompactMode ? expanded : true;
|
||||
|
||||
// Going from collapsed to expanded realizes the (previously collapsed) content
|
||||
// page for the first time, which fires its deferred Loaded event. Suppress the
|
||||
// resulting focus/select so we don't select-all the character the user just typed.
|
||||
if (!this.ExpandedMode && newExpanded)
|
||||
{
|
||||
_suppressSelectOnNextLoad = true;
|
||||
}
|
||||
|
||||
this.ExpandedMode = newExpanded;
|
||||
PropertyChanged?.Invoke(this, new(nameof(ExpandedMode)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces the shell into its compact (collapsed) layout and flushes layout so the host can
|
||||
/// read the resulting card height. Only has an effect when compact mode is enabled.
|
||||
/// </summary>
|
||||
public void EnsureCompactLayout()
|
||||
{
|
||||
var settings = App.Current.Services.GetRequiredService<ISettingsService>().Settings;
|
||||
if (!settings.CompactMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.ExpandedMode = false;
|
||||
PropertyChanged?.Invoke(this, new(nameof(ExpandedMode)));
|
||||
this.UpdateLayout();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Microsoft.CmdPal.UI.Settings.GeneralPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
@@ -119,6 +119,29 @@
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsExpander x:Uid="Settings_GeneralPage_CompactMode_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_GeneralPage_CompactMode" IsOn="{x:Bind viewModel.CompactMode, Mode=TwoWay}" />
|
||||
<controls:SettingsExpander.Items>
|
||||
<controls:SettingsCard
|
||||
x:Uid="Settings_GeneralPage_CompactCenterHeight_SettingsCard"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind viewModel.CompactMode, Mode=OneWay}">
|
||||
<Slider
|
||||
Width="100"
|
||||
Height="100"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
AutomationProperties.AutomationId="CmdPal_GeneralPage_CompactCenterHeight"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Orientation="Vertical"
|
||||
StepFrequency="5"
|
||||
TickFrequency="25"
|
||||
TickPlacement="Outside"
|
||||
Value="{x:Bind viewModel.CompactCenterHeightPercentage, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<!-- 'Behavior' section -->
|
||||
|
||||
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
@@ -82,6 +82,17 @@
|
||||
Click="ToggleDevRibbonClicked"
|
||||
Content="Toggle dev ribbon" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard
|
||||
x:Name="ShowHwndFrameSettingsCard"
|
||||
Description="Shows the OS-drawn title bar, border, and rounded corners on the Command Palette's HWND so its actual bounds are visible. Always off in CI / release builds."
|
||||
Header="Show HWND frame"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch
|
||||
x:Name="ShowHwndFrameToggle"
|
||||
AutomationProperties.AutomationId="CmdPal_InternalPage_ShowHwndFrame"
|
||||
IsOn="{x:Bind ShowHwndFrame, Mode=OneTime}"
|
||||
Toggled="ShowHwndFrameToggle_Toggled" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Gallery Section -->
|
||||
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Extension Gallery" />
|
||||
|
||||
@@ -26,6 +26,8 @@ public sealed partial class InternalPage : Page
|
||||
|
||||
public string GalleryFeedUrl => _settingsService.Settings.GalleryFeedUrl ?? string.Empty;
|
||||
|
||||
public bool ShowHwndFrame => _settingsService.Settings.ShowHwndFrame;
|
||||
|
||||
public InternalPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -120,4 +122,16 @@ public sealed partial class InternalPage : Page
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new ToggleDevRibbonMessage());
|
||||
}
|
||||
|
||||
private void ShowHwndFrameToggle_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ToggleSwitch toggle)
|
||||
{
|
||||
var newValue = toggle.IsOn;
|
||||
if (newValue != _settingsService.Settings.ShowHwndFrame)
|
||||
{
|
||||
_settingsService.UpdateSettings(s => s with { ShowHwndFrame = newValue });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,6 +380,18 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_HighlightSearch_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Selects the previous search text at launch</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_CompactMode_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Compact mode</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_CompactMode_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Shrinks the palette to just the search box until you start typing</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_CompactCenterHeight_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Search box position</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_CompactCenterHeight_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Relative height from the bottom of the screen where the collapsed search box is centered. Only applies in compact mode.</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_KeepPreviousQuery_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Keep previous query</value>
|
||||
</data>
|
||||
|
||||
Reference in New Issue
Block a user