mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-07 02:49:14 +02:00
Compare commits
18 Commits
gleb/advan
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da880280be | ||
|
|
8d6a8e8a9e | ||
|
|
8dc4598786 | ||
|
|
fa8fbb60a1 | ||
|
|
ab4947579b | ||
|
|
d221f84d8f | ||
|
|
14152672c5 | ||
|
|
5688441127 | ||
|
|
9f82e018f6 | ||
|
|
0c2bae21d4 | ||
|
|
c914bdf509 | ||
|
|
3851e06f45 | ||
|
|
b27ca5a9bf | ||
|
|
63701c9b91 | ||
|
|
cbb5e985c9 | ||
|
|
017e3278d9 | ||
|
|
e834031c7f | ||
|
|
981c1cefba |
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@@ -677,6 +677,7 @@ HDEVNOTIFY
|
||||
hdr
|
||||
HDROP
|
||||
hdwwiz
|
||||
Headered
|
||||
Helpline
|
||||
helptext
|
||||
hgdiobj
|
||||
|
||||
229
.github/skills/wpf-to-winui3-migration/SKILL.md
vendored
229
.github/skills/wpf-to-winui3-migration/SKILL.md
vendored
@@ -31,7 +31,9 @@ Migrate PowerToys modules from WPF (`System.Windows.*`) to WinUI 3 (`Microsoft.U
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Recommended Order
|
||||
### Phase-by-Phase Scope
|
||||
|
||||
Work on bounded problems, not the entire codebase at once. Each phase should compile before moving to the next.
|
||||
|
||||
1. **Project file** — Update TFM, NuGet packages, set `<UseWinUI>true</UseWinUI>`
|
||||
2. **Data models and business logic** — No UI dependencies, migrate first
|
||||
@@ -45,79 +47,213 @@ Migrate PowerToys modules from WPF (`System.Windows.*`) to WinUI 3 (`Microsoft.U
|
||||
10. **Installer & build pipeline** — Update WiX, signing, build events
|
||||
11. **Tests** — Adapt for WinUI 3 runtime, async patterns
|
||||
|
||||
### Key Principles
|
||||
### Migration Contract: Prohibited Patterns
|
||||
|
||||
- **Do NOT overwrite `App.xaml` / `App.xaml.cs`** — WinUI 3 has different application lifecycle boilerplate. Merge your resources and initialization code into the generated WinUI 3 App class.
|
||||
- **Do NOT create Exe→WinExe `ProjectReference`** — Extract shared code to a Library project. This causes phantom build artifacts.
|
||||
- **Use `Lazy<T>` for resource-dependent statics** — `ResourceLoader` is not available at class-load time in all contexts.
|
||||
These rules capture human judgment and must be applied consistently across every file. Do NOT deviate.
|
||||
|
||||
**Architecture prohibitions:**
|
||||
- **Do NOT overwrite `App.xaml` / `App.xaml.cs`** — WinUI 3 has different lifecycle boilerplate. Merge resources and init code into the generated WinUI 3 App class.
|
||||
- **Do NOT create Exe→WinExe `ProjectReference`** — Extract shared code to a Library project. Causes phantom build artifacts.
|
||||
- **Do NOT instantiate services directly** — Use DI and CommunityToolkit.Mvvm patterns.
|
||||
- **Do NOT create a `Window` subclass for every dialog or sub-page** — use `ContentDialog` for in-app dialogs and `Frame`/`Page` navigation for sub-views. Separate `Window` classes are reserved for distinct top-level surfaces (e.g., FancyZones editor, OOBE).
|
||||
- **Do NOT omit `WindowsPackageType=None` and `WindowsAppSDKSelfContained=true`** — Both are mandatory in the csproj for every WinUI 3 module in PowerToys. Without them the app crashes at startup with `COMException: ClassFactory cannot supply requested class` because the WinUI 3 runtime DLLs are not found.
|
||||
|
||||
**XAML prohibitions:**
|
||||
- **Do NOT use `{DynamicResource}`** — Replace with `{ThemeResource}` (theme-reactive) or `{StaticResource}`.
|
||||
- **Do NOT use `{Binding}` in `Setter.Value`** — Not supported in WinUI 3. Use `{StaticResource}`.
|
||||
- **Do NOT use `{x:Static}`** — Replace with `{x:Bind}`, `x:Uid`, or code-behind.
|
||||
- **Do NOT use `{x:Type}`** — Not supported. Use `x:DataType` for DataTemplate, or code-behind.
|
||||
- **Do NOT use `clr-namespace:`** — Replace with `using:` in all xmlns declarations.
|
||||
- **Do NOT use `Style.Triggers` / `DataTrigger` / `EventTrigger`** — Replace with `VisualStateManager`.
|
||||
- **Do NOT use `MultiBinding`** — Replace with `x:Bind` function binding or computed ViewModel property.
|
||||
- **Do NOT use `Visibility="Hidden"`** — WinUI only has `Visible` and `Collapsed`. Use `Opacity="0"` if layout must be preserved.
|
||||
- **Do NOT use `IsDefault` / `IsCancel`** — Use `AccentButtonStyle` for primary button; handle Enter/Escape in code-behind.
|
||||
- **Do NOT omit `BasedOn` when overriding default styles** — Without it, your style replaces the entire default. Always use `BasedOn="{StaticResource DefaultButtonStyle}"` etc.
|
||||
- **Do NOT omit `XamlControlsResources` as first merged dictionary** — It provides default Fluent styles. Without it, controls have no visual appearance.
|
||||
|
||||
**Code-behind prohibitions:**
|
||||
- **Do NOT use `Application.Current.Dispatcher`** — Store `DispatcherQueue` in a static field explicitly.
|
||||
- **Do NOT use `Window.Current`** — Not supported. Use a custom `App.Window` static property.
|
||||
- **Do NOT put `DataContext`, `Resources`, or `VisualStateManager` on `Window`** — WinUI 3 `Window` is NOT a `DependencyObject`. Use a root `Page`/`UserControl`/`Grid`.
|
||||
- **Do NOT use tunneling/preview events** (`PreviewMouseDown`, `PreviewKeyDown`) — WinUI has no tunneling. Use bubbling equivalents with `Handled` property or `AddHandler(handledEventsToo: true)`.
|
||||
|
||||
**Resource prohibitions:**
|
||||
- **Do NOT use `Properties.Resources.MyString`** — Replace with `ResourceLoaderInstance.ResourceLoader.GetString("MyString")`.
|
||||
- **Do NOT initialize `ResourceLoader`-dependent values as static fields** — Wrap in `Lazy<T>` or null-coalescing property.
|
||||
- **Do NOT use `pack://` URIs** — Replace with `ms-appx:///` scheme.
|
||||
|
||||
## Quick Reference Tables
|
||||
|
||||
### Namespace Mapping
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` (UI) / `Windows.Graphics.Imaging` (processing) |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` |
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` | Root namespace |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` | Core controls |
|
||||
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` | Low-level primitives |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` | Brushes, transforms |
|
||||
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` | Storyboard, animations |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` (UI) / `Windows.Graphics.Imaging` (processing) | Split by purpose |
|
||||
| `System.Windows.Media.Media3D` | **No equivalent** | Use Win2D or Composition APIs |
|
||||
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` | Rectangle, Ellipse, Path |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` | Pointer, keyboard, focus |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` | Binding, IValueConverter |
|
||||
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` | Limited — RichTextBlock + Paragraph |
|
||||
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` | XAML parsing, markup extensions |
|
||||
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` | Accessibility / UI Automation |
|
||||
| `System.Windows.Navigation` | **No direct equivalent** | Use `Frame.Navigate()` |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` | Dispatcher → DispatcherQueue |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` / `Microsoft.UI.Xaml.Hosting` | HWND interop |
|
||||
|
||||
### Control Replacements (No 1:1 Mapping)
|
||||
|
||||
These WPF controls have no direct counterpart and require a different control or third-party package:
|
||||
|
||||
| WPF Control | WinUI 3 Replacement | Notes |
|
||||
|-------------|---------------------|-------|
|
||||
| `DataGrid` | [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) | Community library; the Toolkit `DataGrid` is no longer maintained. Legacy code may still pin v7 `CommunityToolkit.WinUI.UI.Controls.DataGrid` 7.1.2 |
|
||||
| `Ribbon` | `CommandBar` / `NavigationView`, or [Toolkit Labs Ribbon](https://github.com/CommunityToolkit/Labs-Windows/tree/main/components/Ribbon) | No first-party Ribbon in WinUI; Labs component is experimental/partial |
|
||||
| `Menu` / `MenuItem` | `MenuBar` / `MenuBarItem` / `MenuFlyout` | `MenuBar` for classic menu, `MenuFlyout` for context |
|
||||
| `ContextMenu` | `MenuFlyout` | Assign to `ContextFlyout` property |
|
||||
| `ToolBar` / `ToolBarTray` | `CommandBar` + `AppBarButton` | |
|
||||
| `StatusBar` | Custom `Grid`/`StackPanel` or `InfoBar` | No StatusBar control |
|
||||
| `TabControl` | `TabView` or `NavigationView` (top mode) | `TabView` for closeable tabs |
|
||||
| `DocumentViewer` | `WebView2` | Render PDFs/XPS inside WebView2 |
|
||||
| `FlowDocument` | `RichTextBlock` | Partial replacement only |
|
||||
| `RichTextBox` | `RichEditBox` | Rich text editing |
|
||||
| `GroupBox` | `Expander` (built-in) or `HeaderedContentControl` (Toolkit) | See [Layout & Header Controls from CommunityToolkit.WinUI](#layout--header-controls-from-communitytoolkitwinui) below |
|
||||
| `Label` | `TextBlock` | WPF `Label` is a `ContentControl`; use `TextBlock` + `AccessKey` |
|
||||
| `TreeView` | `TreeView` (native) | Available natively, but data binding model differs significantly |
|
||||
| `MessageBox` | `ContentDialog` | Must set `XamlRoot` before `ShowAsync()` |
|
||||
| `MediaElement` | `MediaPlayerElement` | Different API |
|
||||
| `AccessText` | Not available | Use `AccessKey` property on target control |
|
||||
|
||||
### Layout & Header Controls from CommunityToolkit.WinUI
|
||||
|
||||
These WPF controls have no built-in WinUI 3 equivalent — install the corresponding CommunityToolkit package. **The NuGet package id and the XAML namespace differ intentionally**: package names end in `.Primitives` / `.HeaderedControls`, but the registered XAML namespace is the shorter `CommunityToolkit.WinUI.Controls` (confirmed in the [official Microsoft Q&A](https://learn.microsoft.com/en-us/answers/questions/5746230/why-does-communitytoolkit-uwp-controls-primitive-u)).
|
||||
|
||||
| WPF Control | WinUI 3 Replacement | NuGet Package | XAML Namespace |
|
||||
|-------------|---------------------|---------------|----------------|
|
||||
| `WrapPanel` | `WrapPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `UniformGrid` | `UniformGrid` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `DockPanel` | `DockPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `GroupBox` (alt.) | `HeaderedContentControl` | `CommunityToolkit.WinUI.Controls.HeaderedControls` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
|
||||
### No Equivalent — Requires Architectural Rework
|
||||
|
||||
These WPF features have no WinUI counterpart and require redesign, not find-and-replace:
|
||||
|
||||
| WPF Feature | WinUI 3 Replacement Strategy |
|
||||
|-------------|------------------------------|
|
||||
| `Style.Triggers` / `DataTrigger` | `VisualStateManager` with `StateTrigger` — see [XAML Migration](./references/xaml-migration.md) |
|
||||
| `MultiBinding` | `x:Bind` function binding: `{x:Bind local:Converters.Format(VM.A, VM.B), Mode=OneWay}` |
|
||||
| `RoutedUICommand` / `CommandBinding` | `ICommand` / `[RelayCommand]` from CommunityToolkit.Mvvm. WinUI also has `StandardUICommand` / `XamlUICommand` for platform commands. |
|
||||
| `AdornerLayer` / `Adorner` | Depends on use case: `TeachingTip`/`InfoBar` (validation), `Popup` (overlays), `PlaceholderText` (watermarks), Canvas overlay (decorations) |
|
||||
| `Visibility.Hidden` | `Opacity="0"` with `Visibility="Visible"` (preserves layout space) |
|
||||
| `Window.Resources` / `Window.DataContext` | Move to root `Grid.Resources` / root `Page`/`UserControl` — WinUI `Window` is NOT a DependencyObject |
|
||||
| Tunneling events (`Preview*`) | Use bubbling equivalents + `Handled` property or `AddHandler(handledEventsToo: true)` |
|
||||
|
||||
### Critical API Replacements
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `Dispatcher.Invoke()` | `DispatcherQueue.TryEnqueue()` | Different return type (`bool`) |
|
||||
| `Dispatcher.Invoke()` | `DispatcherQueue.TryEnqueue()` | Different return type (`bool`), async by default |
|
||||
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` | Property vs method |
|
||||
| `Application.Current.Dispatcher` | Store `DispatcherQueue` in static field | See [Threading](./references/threading-and-windowing.md) |
|
||||
| `Window.Current` | Custom `App.Window` static property | Not supported in Windows App SDK |
|
||||
| `Application.Current.MainWindow` | Custom `App.Window` static property | Must track manually |
|
||||
| `MessageBox.Show()` | `ContentDialog` | Must set `XamlRoot` |
|
||||
| `System.Windows.Clipboard` | `Windows.ApplicationModel.DataTransfer.Clipboard` | Different API surface |
|
||||
| `RoutedUICommand` / `CommandBinding` | `ICommand` / `[RelayCommand]` | Remove `CommandBinding`; bind `ICommand` directly |
|
||||
| `Properties.Resources.MyString` | `ResourceLoaderInstance.ResourceLoader.GetString("MyString")` | Lazy-init pattern |
|
||||
| `DynamicResource` | `ThemeResource` | Theme-reactive only |
|
||||
| `clr-namespace:` | `using:` | XAML namespace prefix |
|
||||
| `{x:Static props:Resources.Key}` | `x:Uid` or `ResourceLoader.GetString()` | .resx → .resw |
|
||||
| `DataType="{x:Type m:Foo}"` | Remove or use code-behind | `x:Type` not supported |
|
||||
| `Properties.Resources.MyString` | `ResourceLoaderInstance.ResourceLoader.GetString("MyString")` | Lazy-init pattern |
|
||||
| `Application.Current.MainWindow` | Custom `App.Window` static property | Must track manually |
|
||||
| `DataType="{x:Type m:Foo}"` | `x:DataType="m:Foo"` | `x:Type` not supported |
|
||||
| `SizeToContent="Height"` | Custom `SizeToContent()` via `AppWindow.Resize()` | See [Windowing](./references/threading-and-windowing.md) |
|
||||
| `MouseLeftButtonDown` | `PointerPressed` | Mouse → Pointer events |
|
||||
| `Pack URI (pack://...)` | `ms-appx:///` | Resource URI scheme |
|
||||
| `Observable` (custom base) | `ObservableObject` + `[ObservableProperty]` | CommunityToolkit.Mvvm |
|
||||
| `RelayCommand` (custom) | `[RelayCommand]` source generator | CommunityToolkit.Mvvm |
|
||||
| `JpegBitmapEncoder` | `BitmapEncoder.CreateAsync(JpegEncoderId, stream)` | Async, unified API |
|
||||
| `encoder.QualityLevel = 85` | `BitmapPropertySet { "ImageQuality", 0.85f }` | int 1-100 → float 0-1 |
|
||||
|
||||
### Event Replacements (Mouse → Pointer)
|
||||
|
||||
| WPF Event | WinUI 3 Event | Notes |
|
||||
|-----------|--------------|-------|
|
||||
| `MouseLeftButtonDown` | `PointerPressed` | Check `IsLeftButtonPressed` on args |
|
||||
| `MouseLeftButtonUp` | `PointerReleased` | Check pointer properties |
|
||||
| `MouseRightButtonDown` | `RightTapped` | Or `PointerPressed` with right button check |
|
||||
| `MouseMove` | `PointerMoved` | `MouseEventArgs` → `PointerRoutedEventArgs` |
|
||||
| `MouseWheel` | `PointerWheelChanged` | Different event args |
|
||||
| `MouseEnter` / `MouseLeave` | `PointerEntered` / `PointerExited` | |
|
||||
| `MouseDoubleClick` | `DoubleTapped` | Different event args |
|
||||
| `PreviewMouseDown` | `PointerPressed` | No tunneling — use `Handled` or `AddHandler` |
|
||||
| `PreviewKeyDown` | `KeyDown` | `KeyEventArgs` → `KeyRoutedEventArgs` |
|
||||
|
||||
### Property Replacements
|
||||
|
||||
| WPF | WinUI 3 | Context |
|
||||
|-----|---------|---------|
|
||||
| `Visibility.Hidden` | `Visibility.Collapsed` or `Opacity="0"` | Use `Opacity="0"` to preserve layout |
|
||||
| `TextWrapping.WrapWithOverflow` | `TextWrapping.Wrap` | WinUI doesn't distinguish |
|
||||
| `Focusable="True"` | `IsTabStop="True"` | Different property name |
|
||||
| `ContextMenu=` | `ContextFlyout=` | On any `UIElement` |
|
||||
| `MediaElement` | `MediaPlayerElement` | Different API |
|
||||
| `SnapsToDevicePixels` | Not available | WinUI handles pixel snapping internally |
|
||||
|
||||
### NuGet Package Migration
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` |
|
||||
| `WPF-UI` (Lepo) | Remove — use native WinUI 3 controls |
|
||||
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) |
|
||||
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` |
|
||||
| (none) | `Microsoft.WindowsAppSDK` |
|
||||
| (none) | `Microsoft.Windows.SDK.BuildTools` |
|
||||
| (none) | `WinUIEx` (optional, for window helpers) |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` |
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` | |
|
||||
| `WPF-UI` (Lepo) | **Remove** — use native WinUI 3 controls | |
|
||||
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) | |
|
||||
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` | |
|
||||
| (none) | `Microsoft.WindowsAppSDK` | Required |
|
||||
| (none) | `Microsoft.Windows.SDK.BuildTools` | Required |
|
||||
| (none) | `WinUIEx` | Optional, window helpers |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.Primitives` | Optional — `WrapPanel`, `UniformGrid`, `DockPanel`, `ConstrainedBox` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.HeaderedControls` | Optional — `HeaderedContentControl`, `HeaderedItemsControl`, `HeaderedTreeView` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.SettingsControls` | Optional — `SettingsCard`, `SettingsExpander` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.Sizers` | Optional — `GridSplitter` |
|
||||
| (none) | `CommunityToolkit.WinUI.UI.Controls.DataGrid` | Legacy v7 — only for migrating existing `DataGrid` code; prefer `WinUI.TableView` |
|
||||
|
||||
### XAML Syntax Changes
|
||||
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `xmlns:local="clr-namespace:MyApp"` | `xmlns:local="using:MyApp"` |
|
||||
| `{DynamicResource Key}` | `{ThemeResource Key}` |
|
||||
| `{x:Static Type.Member}` | `{x:Bind}` or code-behind |
|
||||
| `{x:Type local:MyType}` | Not supported |
|
||||
| `<Style.Triggers>` / `<DataTrigger>` | `VisualStateManager` |
|
||||
| `{Binding}` in `Setter.Value` | Not supported — use `StaticResource` |
|
||||
| `Content="{x:Static p:Resources.Cancel}"` | `x:Uid="Cancel"` with `.Content` in `.resw` |
|
||||
| `<ui:FluentWindow>` / `<ui:Button>` (WPF-UI) | Native `<Window>` / `<Button>` |
|
||||
| `<ui:NumberBox>` / `<ui:ProgressRing>` (WPF-UI) | Native `<NumberBox>` / `<ProgressRing>` |
|
||||
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` |
|
||||
| `IsDefault="True"` / `IsCancel="True"` | `Style="{StaticResource AccentButtonStyle}"` / handle via KeyDown |
|
||||
| `<AccessText>` | Not available — use `AccessKey` property |
|
||||
| `<behaviors:Interaction.Triggers>` | Migrate to code-behind or WinUI behaviors |
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `xmlns:local="clr-namespace:MyApp"` | `xmlns:local="using:MyApp"` | CLR → using syntax |
|
||||
| `{DynamicResource Key}` | `{ThemeResource Key}` | Re-evaluates on theme change |
|
||||
| `{StaticResource Key}` | `{StaticResource Key}` | Same — resolved once at load |
|
||||
| `{x:Static Type.Member}` | `{x:Bind}` or code-behind | |
|
||||
| `{x:Type local:MyType}` | Not supported | Use `x:DataType` for DataTemplate |
|
||||
| `{x:Array}` | Not supported | Create collections in code-behind |
|
||||
| `<Style.Triggers>` / `<DataTrigger>` | `VisualStateManager` | See [XAML Migration](./references/xaml-migration.md) |
|
||||
| `{Binding}` in `Setter.Value` | Not supported — use `StaticResource` | |
|
||||
| `Content="{x:Static p:Resources.Cancel}"` | `x:Uid="Cancel"` with `.Content` in `.resw` | |
|
||||
| `sys:String` / `sys:Int32` / etc. | `x:String` / `x:Int32` / etc. | XAML intrinsic types |
|
||||
| `<ui:FluentWindow>` (WPF-UI) | `<Window>` | Native + `ExtendsContentIntoTitleBar` |
|
||||
| `<ui:NumberBox>` / `<ui:ProgressRing>` (WPF-UI) | Native `<NumberBox>` / `<ProgressRing>` | |
|
||||
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` | Named style keys |
|
||||
| `IsDefault="True"` / `IsCancel="True"` | `Style="{StaticResource AccentButtonStyle}"` / KeyDown | |
|
||||
| `<AccessText>` | Not available — use `AccessKey` property | |
|
||||
| `<behaviors:Interaction.Triggers>` | Code-behind or WinUI behaviors | |
|
||||
| `Window.Resources` | Root container's `Resources` (e.g. `Grid.Resources`) | Window is not a DependencyObject |
|
||||
|
||||
### Binding: {Binding} vs {x:Bind}
|
||||
|
||||
Both work in WinUI 3. Prefer `{x:Bind}` for new/migrated code.
|
||||
|
||||
| Feature | `{Binding}` | `{x:Bind}` |
|
||||
|---------|------------|------------|
|
||||
| Default mode | `OneWay` | **`OneTime`** — add `Mode=OneWay` explicitly! |
|
||||
| Default source | `DataContext` | Page/UserControl code-behind |
|
||||
| Compile-time validation | No | Yes |
|
||||
| Function binding | No | Yes (replaces `MultiBinding`) |
|
||||
| Performance | Reflection-based | Compiled, no reflection |
|
||||
| `MultiBinding` support | No (not in WinUI) | Use function binding |
|
||||
|
||||
## Detailed Reference Docs
|
||||
|
||||
@@ -151,6 +287,8 @@ Read only the section relevant to your current task:
|
||||
| JPEG quality value wrong after migration | WPF: int 1-100; WinRT: float 0.0-1.0 |
|
||||
| MSIX packaging fails in PreBuildEvent | Move to PostBuildEvent; artifacts not ready at PreBuild time |
|
||||
| RC file icon path with forward slashes | Use double-backslash escaping: `..\\ui\\Assets\\icon.ico` |
|
||||
| `COMException: ClassFactory cannot supply requested class` at startup | Missing `<WindowsPackageType>None</WindowsPackageType>` and/or `<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>` in csproj. Without these, the app tries to locate the Windows App SDK framework package (not installed) instead of using bundled runtime DLLs. **Both properties are mandatory for every WinUI 3 module in PowerToys.** |
|
||||
| `CombinedGeometry` not available in WinUI 3 | WinUI 3 `UIElement.Clip` only accepts `RectangleGeometry`. For overlay hole effects (exclude region), use a `Path` element with `GeometryGroup FillRule="EvenOdd"` containing two `RectangleGeometry` children — the EvenOdd rule creates a transparent hole where geometries overlap. |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -163,3 +301,4 @@ Read only the section relevant to your current task:
|
||||
| NuGet restore failures | Run `build-essentials.cmd` after adding `Microsoft.WindowsAppSDK` package |
|
||||
| `Parallel.ForEach` compilation error | Migrate to `Parallel.ForEachAsync` for async imaging operations |
|
||||
| Signing check fails on leaked artifacts | Run `generateAllFileComponents.ps1`; verify only `WinUI3Apps\\` paths in signing config |
|
||||
| `COMException` / `ClassFactory` error at app launch | Ensure csproj has `<WindowsPackageType>None</WindowsPackageType>` and `<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>`. These are required for all unpackaged WinUI 3 apps in PowerToys — without them the WinUI 3 COM runtime cannot be found. |
|
||||
|
||||
@@ -4,24 +4,26 @@ Complete reference for mapping WPF types to WinUI 3 equivalents, based on the Im
|
||||
|
||||
## Root Namespace Mapping
|
||||
|
||||
| WPF Namespace | WinUI 3 Namespace |
|
||||
|---------------|-------------------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` |
|
||||
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` |
|
||||
| `System.Windows.Automation.Peers` | `Microsoft.UI.Xaml.Automation.Peers` |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
|
||||
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
|
||||
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
|
||||
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
|
||||
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` |
|
||||
| `System.Windows.Navigation` | `Microsoft.UI.Xaml.Navigation` |
|
||||
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` |
|
||||
| WPF Namespace | WinUI 3 Namespace | Notes |
|
||||
|---------------|-------------------|-------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` | Root namespace |
|
||||
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` | Accessibility / UI Automation |
|
||||
| `System.Windows.Automation.Peers` | `Microsoft.UI.Xaml.Automation.Peers` | |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` | Core controls |
|
||||
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` | Low-level primitives |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` | Binding, IValueConverter |
|
||||
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` | Limited — RichTextBlock + Paragraph only |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` | Pointer, keyboard, focus |
|
||||
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` | XAML parsing, markup extensions |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` | Brushes, transforms |
|
||||
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` | Storyboard, animations |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` | UI display only — use `Windows.Graphics.Imaging` for processing |
|
||||
| `System.Windows.Media.Media3D` | **No equivalent** | Use Win2D or Composition APIs |
|
||||
| `System.Windows.Navigation` | `Microsoft.UI.Xaml.Navigation` | For Frame navigation events; no `NavigationService` |
|
||||
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` | Rectangle, Ellipse, Path |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` | Dispatcher → DispatcherQueue |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` | HWND interop |
|
||||
| `System.Windows.Interop` | `Microsoft.UI.Xaml.Hosting` | XAML Islands |
|
||||
|
||||
## Core Type Mapping
|
||||
|
||||
@@ -45,7 +47,7 @@ Complete reference for mapping WPF types to WinUI 3 equivalents, based on the Im
|
||||
|
||||
These controls exist in both frameworks with the same name — change `System.Windows.Controls` to `Microsoft.UI.Xaml.Controls`:
|
||||
|
||||
`Button`, `TextBox`, `TextBlock`, `ComboBox`, `CheckBox`, `ListBox`, `ListView`, `Image`, `StackPanel`, `Grid`, `Border`, `ScrollViewer`, `ContentControl`, `UserControl`, `Page`, `Frame`, `Slider`, `ProgressBar`, `ToolTip`, `RadioButton`, `ToggleButton`
|
||||
`Button`, `TextBox`, `TextBlock`, `ComboBox`, `CheckBox`, `ListView`, `Image`, `StackPanel`, `Grid`, `Border`, `ScrollViewer`, `ContentControl`, `UserControl`, `Page`, `Frame`, `Slider`, `ProgressBar`, `ToolTip`, `RadioButton`, `ToggleButton`
|
||||
|
||||
### Controls With Different Names or Behavior
|
||||
|
||||
@@ -56,6 +58,7 @@ These controls exist in both frameworks with the same name — change `System.Wi
|
||||
| `TabControl` | `TabView` | Different API |
|
||||
| `Menu` | `MenuBar` | Different API |
|
||||
| `StatusBar` | Custom `StackPanel` layout | No built-in equivalent |
|
||||
| `ListBox` | `ListView` (or `ItemsView`) | **Deprecated in WinUI 3 — do not use.** Prefer `ListView`, or `ItemsView` (WinUI 1.5+) for modern collection scenarios |
|
||||
| `AccessText` | Not available | Use `AccessKey` property on target control |
|
||||
|
||||
### WPF-UI (Lepo) to Native WinUI 3
|
||||
@@ -109,6 +112,43 @@ Last parameter changes from `CultureInfo` to `string` (BCP-47 language tag). All
|
||||
| `System.Windows.Interop.WindowInteropHelper` | `WinRT.Interop.WindowNative.GetWindowHandle()` | |
|
||||
| `System.Windows.SystemColors` | Resource keys via `ThemeResource` | No direct static class |
|
||||
| `System.Windows.SystemParameters` | Win32 API or `DisplayInformation` | No direct equivalent |
|
||||
| `System.Windows.Clipboard` | `Windows.ApplicationModel.DataTransfer.Clipboard` | Different API surface |
|
||||
| `System.Windows.Input.RoutedUICommand` | `Microsoft.UI.Xaml.Input.StandardUICommand` / `XamlUICommand` | Or use `ICommand` / `[RelayCommand]` |
|
||||
| `System.Windows.Input.CommandBinding` | **Remove** | Bind `ICommand` directly in XAML |
|
||||
|
||||
## Controls That Need Translation (No 1:1 Mapping)
|
||||
|
||||
These controls exist in WPF but require a different control, third-party library, or Community Toolkit package in WinUI 3:
|
||||
|
||||
| WPF Control | WinUI 3 Replacement | Package / Notes |
|
||||
|-------------|---------------------|-----------------|
|
||||
| `DataGrid` | [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) | Community library; the Toolkit `DataGrid` is no longer maintained. PowerToys legacy modules may still pin v7 `CommunityToolkit.WinUI.UI.Controls.DataGrid` 7.1.2 — prefer `WinUI.TableView` for new work. |
|
||||
| `Ribbon` | `CommandBar` / `NavigationView`, or [Toolkit Labs Ribbon](https://github.com/CommunityToolkit/Labs-Windows/tree/main/components/Ribbon) | No first-party Ribbon in WinUI; the Labs component is experimental and partial |
|
||||
| `Menu` / `MenuItem` | `MenuBar` / `MenuBarItem` / `MenuFlyoutItem` | `MenuBar` for classic menu, `MenuFlyout` for context |
|
||||
| `ContextMenu` | `MenuFlyout` | Assign to `ContextFlyout` property |
|
||||
| `ToolBar` / `ToolBarTray` | `CommandBar` + `AppBarButton` | |
|
||||
| `StatusBar` | Custom `Grid`/`StackPanel` or `InfoBar` | No StatusBar control |
|
||||
| `TabControl` | `TabView` or `NavigationView` (top mode) | `TabView` for closeable tabs |
|
||||
| `DocumentViewer` | `WebView2` | `Microsoft.Web.WebView2` — render PDFs/XPS |
|
||||
| `FlowDocument` | `RichTextBlock` | Partial replacement only |
|
||||
| `RichTextBox` | `RichEditBox` | Rich text editing |
|
||||
| `GroupBox` | `Expander` (built-in) or `HeaderedContentControl` (Toolkit) | See [Layout & Header Controls from CommunityToolkit.WinUI](#layout--header-controls-from-communitytoolkitwinui) below |
|
||||
| `Label` | `TextBlock` | WPF `Label` is a `ContentControl`; use `TextBlock` + `AccessKey` |
|
||||
| `TreeView` | `TreeView` (native) | Available natively, but data binding model differs significantly |
|
||||
| `MediaElement` | `MediaPlayerElement` | Different API |
|
||||
|
||||
## Layout & Header Controls from CommunityToolkit.WinUI
|
||||
|
||||
These WPF layout/header controls have no built-in WinUI 3 equivalent — install the corresponding CommunityToolkit package. **The NuGet package id and the XAML namespace differ intentionally**: package names end in `.Primitives` / `.HeaderedControls`, but both packages register their controls in the shorter `CommunityToolkit.WinUI.Controls` XAML namespace (confirmed in the [official Microsoft Q&A](https://learn.microsoft.com/en-us/answers/questions/5746230/why-does-communitytoolkit-uwp-controls-primitive-u): *"all controls live under `CommunityToolkit.WinUI.Controls` … This is intentional"*).
|
||||
|
||||
| WPF Control | WinUI 3 Replacement | NuGet Package | XAML Namespace |
|
||||
|-------------|---------------------|---------------|----------------|
|
||||
| `WrapPanel` | `WrapPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `UniformGrid` | `UniformGrid` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `DockPanel` | `DockPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `GroupBox` (alt.) | `HeaderedContentControl` | `CommunityToolkit.WinUI.Controls.HeaderedControls` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
|
||||
Other primitives in `Primitives`: `ConstrainedBox`, `SwitchPresenter`, `WrapLayout`, `StaggeredPanel`. Other Headered controls in `HeaderedControls`: `HeaderedItemsControl`, `HeaderedTreeView`.
|
||||
|
||||
## NuGet Package Migration
|
||||
|
||||
@@ -124,6 +164,11 @@ Last parameter changes from `CultureInfo` to `string` (BCP-47 language tag). All
|
||||
| (none) | `WinUIEx` | Optional, window helpers |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
|
||||
| (none) | `CommunityToolkit.WinUI.Extensions` | Optional |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.Primitives` | Optional — `WrapPanel`, `UniformGrid`, `DockPanel`, `ConstrainedBox`, `SwitchPresenter` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.HeaderedControls` | Optional — `HeaderedContentControl`, `HeaderedItemsControl`, `HeaderedTreeView` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.SettingsControls` | Optional — `SettingsCard`, `SettingsExpander` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.Sizers` | Optional — `GridSplitter`, `PropertySizer` |
|
||||
| (none) | `CommunityToolkit.WinUI.UI.Controls.DataGrid` | Legacy v7 — only if migrating existing `DataGrid` code; prefer [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) for new work |
|
||||
| (none) | `Microsoft.Web.WebView2` | If using WebView |
|
||||
|
||||
## Project File Changes
|
||||
@@ -167,8 +212,9 @@ Last parameter changes from `CultureInfo` to `string` (BCP-47 language tag). All
|
||||
Key changes:
|
||||
- `UseWPF` → `UseWinUI`
|
||||
- TFM: `net8.0-windows` → `net8.0-windows10.0.19041.0`
|
||||
- Add `WindowsPackageType=None` for unpackaged desktop apps
|
||||
- Add `SelfContained=true` + `WindowsAppSDKSelfContained=true`
|
||||
- **CRITICAL: Add `WindowsPackageType=None`** — marks the app as unpackaged (no MSIX). Without this, the build produces an MSIX-style package that won't run as a standalone PowerToys module.
|
||||
- **CRITICAL: Add `WindowsAppSDKSelfContained=true`** — bundles the Windows App SDK runtime DLLs (e.g. `Microsoft.UI.Xaml.dll`) into the output directory. Without this, the app throws `COMException: ClassFactory cannot supply requested class` at startup because the WinUI 3 COM classes cannot be found.
|
||||
- Add `SelfContained=true` (usually via `Common.SelfContained.props`)
|
||||
- Add `DISABLE_XAML_GENERATED_MAIN` if using custom `Program.cs` entry point
|
||||
- Set `ProjectPriFileName` to match your module's assembly name
|
||||
- Move icon from `Resources/` to `Assets/<Module>/`
|
||||
|
||||
@@ -127,6 +127,74 @@ If the module uses the `WPF-UI` library, replace all Lepo controls with native W
|
||||
</Window>
|
||||
```
|
||||
|
||||
#### Recommended: Use WindowEx from WinUIEx
|
||||
|
||||
> **Tip:** Prefer `WinUIEx.WindowEx` over bare `Window`. It restores many WPF-like window properties directly in XAML, avoiding boilerplate code-behind for common windowing tasks.
|
||||
|
||||
```xml
|
||||
<!-- WinUI 3 with WindowEx (preferred in PowerToys) -->
|
||||
<winuiex:WindowEx
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
x:Class="MyApp.MainWindow"
|
||||
MinWidth="480"
|
||||
MinHeight="320"
|
||||
IsShownInSwitchers="True"
|
||||
IsTitleBarVisible="True">
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
<Grid>
|
||||
...
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
```
|
||||
|
||||
Properties available on `WindowEx` that mirror WPF `Window`:
|
||||
|
||||
| WPF Window Property | WindowEx Property | Notes |
|
||||
|---------------------|-------------------|-------|
|
||||
| `MinWidth` / `MinHeight` | `MinWidth` / `MinHeight` | Set directly in XAML |
|
||||
| `Width` / `Height` | `Width` / `Height` | Initial window size |
|
||||
| `WindowState` | `WindowState` | Minimized, Maximized, Normal |
|
||||
| `Title` | `Title` or `x:Uid` | Window title |
|
||||
| `Icon` | Use `TitleBar.IconSource` | Via WinUI TitleBar control |
|
||||
| `ShowInTaskbar` | `IsShownInSwitchers` | Alt-Tab visibility |
|
||||
| `TopMost` | `IsAlwaysOnTop` | Always-on-top window |
|
||||
|
||||
NuGet: `WinUIEx` — already referenced by most PowerToys modules.
|
||||
|
||||
#### Recommended: Page-in-Window Architecture
|
||||
|
||||
> **Tip:** WinUI 3 `Window` is NOT a `FrameworkElement` — it does not support `Resources`, `DataContext`, `x:Bind`, or `VisualStateManager` directly. Place a `Page` as the Window's root content to regain these WPF-like capabilities.
|
||||
|
||||
```xml
|
||||
<!-- WinUI 3 — Window contains a Page for full FrameworkElement support -->
|
||||
<winuiex:WindowEx x:Class="MyApp.MainWindow"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
xmlns:views="using:MyApp.Views">
|
||||
<views:MainPage x:Name="mainPage" />
|
||||
</winuiex:WindowEx>
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- MainPage.xaml — has full FrameworkElement capabilities -->
|
||||
<Page x:Class="MyApp.Views.MainPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<Page.Resources>
|
||||
<!-- Resources work here (unlike on Window) -->
|
||||
<SolidColorBrush x:Key="MyBrush" Color="Red"/>
|
||||
</Page.Resources>
|
||||
<Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<!-- VisualStateManager works here (unlike on Window) -->
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
...
|
||||
</Grid>
|
||||
</Page>
|
||||
```
|
||||
|
||||
This is the standard pattern in PowerToys (e.g., FileLocksmith, EnvironmentVariables).
|
||||
|
||||
### App.xaml Resources
|
||||
|
||||
```xml
|
||||
@@ -150,6 +218,20 @@ If the module uses the `WPF-UI` library, replace all Lepo controls with native W
|
||||
</Application.Resources>
|
||||
```
|
||||
|
||||
### CommunityToolkit.WinUI — WPF Replacement Controls
|
||||
|
||||
> **Tip:** The `CommunityToolkit.WinUI` package provides many controls and helpers familiar to WPF developers that are missing from WinUI 3 out of the box. Before writing custom replacements, check whether CommunityToolkit already provides what you need.
|
||||
|
||||
Key packages (XAML namespace is `using:CommunityToolkit.WinUI.Controls` for the `Controls.*` family):
|
||||
- **`CommunityToolkit.WinUI.Controls.Primitives`** — `WrapPanel`, `UniformGrid`, `DockPanel`, `ConstrainedBox`, `SwitchPresenter`
|
||||
- **`CommunityToolkit.WinUI.Controls.HeaderedControls`** — `HeaderedContentControl`, `HeaderedItemsControl`, `HeaderedTreeView`
|
||||
- **`CommunityToolkit.WinUI.Controls.SettingsControls`** — `SettingsCard`, `SettingsExpander`
|
||||
- **`CommunityToolkit.WinUI.Controls.Sizers`** — `GridSplitter`, `PropertySizer`, `ContentSizer`
|
||||
- **`CommunityToolkit.WinUI.UI.Controls.DataGrid`** — legacy v7 `DataGrid` (no longer maintained); prefer [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) for new work
|
||||
- **`CommunityToolkit.WinUI.Converters`** — Common value converters (`BoolToVisibilityConverter`, `StringFormatConverter`, etc.)
|
||||
- **`CommunityToolkit.WinUI.Behaviors`** — XAML behaviors for animations and interactions
|
||||
- **`CommunityToolkit.WinUI.Extensions`** — Extension methods for WinUI types
|
||||
|
||||
### Common Control Replacements
|
||||
|
||||
```xml
|
||||
@@ -187,11 +269,135 @@ If the module uses the `WPF-UI` library, replace all Lepo controls with native W
|
||||
<!-- Handle Enter/Escape keys in code-behind if needed -->
|
||||
```
|
||||
|
||||
## No-Equivalent Patterns (Requires Architectural Rework)
|
||||
|
||||
These WPF features demand design changes, not find-and-replace. Read this section BEFORE attempting to migrate any file that uses these patterns.
|
||||
|
||||
### MultiBinding → x:Bind Function Binding
|
||||
|
||||
WinUI does not support `MultiBinding`. Replace with `x:Bind` function binding (most direct replacement), a computed ViewModel property, or multiple simple bindings.
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0} {1}">
|
||||
<Binding Path="FirstName" />
|
||||
<Binding Path="LastName" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
```
|
||||
|
||||
**WinUI 3:**
|
||||
```xml
|
||||
<TextBlock Text="{x:Bind local:Converters.FormatFullName(ViewModel.FirstName, ViewModel.LastName), Mode=OneWay}" />
|
||||
```
|
||||
|
||||
```csharp
|
||||
public static class Converters
|
||||
{
|
||||
public static string FormatFullName(string first, string last) => $"{first} {last}";
|
||||
}
|
||||
```
|
||||
|
||||
### Adorners → Context-Dependent Replacements
|
||||
|
||||
WPF's `AdornerLayer` has no WinUI equivalent. Choose replacement by use case:
|
||||
|
||||
| Adorner Use Case | WinUI 3 Replacement |
|
||||
|------------------|---------------------|
|
||||
| Validation indicators | `TeachingTip`, `InfoBar`, or InputValidation templates |
|
||||
| Resize handles | `Popup` positioned relative to target |
|
||||
| Drag preview | `DragItemsStarting` event with custom DragUI |
|
||||
| Overlay decorations | Canvas overlay or Popup layer |
|
||||
| Watermark / Placeholder | `TextBox.PlaceholderText` (built-in) |
|
||||
|
||||
### RoutedUICommand → ICommand / RelayCommand
|
||||
|
||||
WinUI does not support routed commands or `CommandBinding`. Replace with standard `ICommand` pattern:
|
||||
|
||||
```csharp
|
||||
// CommunityToolkit.Mvvm
|
||||
[RelayCommand(CanExecute = nameof(CanSave))]
|
||||
private void Save() { /* save logic */ }
|
||||
private bool CanSave() => IsDirty;
|
||||
```
|
||||
|
||||
WinUI 3 also provides `StandardUICommand` and `XamlUICommand` for pre-defined platform commands (Cut, Copy, Paste, Delete) with built-in icons and keyboard accelerators.
|
||||
|
||||
### Tunneling / Preview Events
|
||||
|
||||
WinUI has no tunneling event model. `PreviewMouseDown`, `PreviewKeyDown`, etc. do not exist.
|
||||
|
||||
- Replace with the bubbling equivalent (`PointerPressed`, `KeyDown`)
|
||||
- If you relied on tunneling to intercept events before children, restructure using the `Handled` property
|
||||
- For must-handle scenarios, use `AddHandler` with `handledEventsToo: true`:
|
||||
```csharp
|
||||
myElement.AddHandler(UIElement.PointerPressedEvent,
|
||||
new PointerEventHandler(OnPointerPressed), handledEventsToo: true);
|
||||
```
|
||||
|
||||
## Style and Template Changes
|
||||
|
||||
### Implicit Styles — Always Use BasedOn
|
||||
|
||||
> **Warning:** In WinUI 3, always use `BasedOn` when overriding default control styles. Without it, your style **replaces the entire default style** rather than extending it.
|
||||
|
||||
```xml
|
||||
<!-- WRONG — replaces entire default style, control may lose all visual appearance -->
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="Red" />
|
||||
</Style>
|
||||
|
||||
<!-- CORRECT — extends the default style -->
|
||||
<Style TargetType="Button" BasedOn="{StaticResource DefaultButtonStyle}">
|
||||
<Setter Property="Background" Value="Red" />
|
||||
</Style>
|
||||
```
|
||||
|
||||
### Triggers → VisualStateManager
|
||||
|
||||
WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported.
|
||||
WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported. Two replacement approaches:
|
||||
|
||||
#### Approach 1: StateTrigger (direct DataTrigger replacement — simpler)
|
||||
|
||||
Use this for data-driven state changes. This is the closest equivalent to WPF `DataTrigger`:
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
<Style TargetType="Border">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsActive}" Value="True">
|
||||
<Setter Property="Background" Value="Green" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
```
|
||||
|
||||
**WinUI 3:**
|
||||
```xml
|
||||
<Border x:Name="MyBorder">
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup>
|
||||
<VisualState x:Name="Active">
|
||||
<VisualState.StateTriggers>
|
||||
<StateTrigger IsActive="{x:Bind ViewModel.IsActive, Mode=OneWay}" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="MyBorder.Background" Value="Green" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Border>
|
||||
```
|
||||
|
||||
Note: `VisualStateManager` must be placed on a control inside the Window, NOT on the Window itself.
|
||||
|
||||
#### Approach 2: ControlTemplate (for property triggers like IsMouseOver)
|
||||
|
||||
Use this when replacing `<Trigger Property="IsMouseOver">` or similar control-state triggers:
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
@@ -200,9 +406,6 @@ WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported.
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="LightBlue"/>
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
```
|
||||
@@ -249,11 +452,42 @@ WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported.
|
||||
| `Disabled` | `Disabled` |
|
||||
| `Pressed` | `Pressed` |
|
||||
|
||||
## Visibility.Hidden — No Equivalent
|
||||
|
||||
WinUI only has `Visible` and `Collapsed`. There is no `Hidden`.
|
||||
|
||||
| WPF | WinUI 3 | Behavior |
|
||||
|-----|---------|----------|
|
||||
| `Visibility.Visible` | `Visibility.Visible` | Rendered and occupies layout space |
|
||||
| `Visibility.Hidden` | **Not available** | Use `Opacity="0"` with `Visibility="Visible"` to hide but keep layout |
|
||||
| `Visibility.Collapsed` | `Visibility.Collapsed` | Not rendered, no layout space |
|
||||
|
||||
## Resource Dictionary Changes
|
||||
|
||||
### Window.Resources → Grid.Resources
|
||||
### XamlControlsResources Must Be First
|
||||
|
||||
WinUI 3 `Window` is NOT a `DependencyObject` — no `Window.Resources`, `DataContext`, or `VisualStateManager`.
|
||||
> **Warning:** `XamlControlsResources` must be the **first** merged dictionary in `App.xaml`. It provides the default Fluent styles. Omitting it gives you controls with no visual appearance. Resource paths use `ms-appx:///` instead of relative paths.
|
||||
|
||||
```xml
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- MUST be first -->
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Then your custom dictionaries -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Colors.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
```
|
||||
|
||||
### Window.Resources → Grid.Resources (or use Page)
|
||||
|
||||
WinUI 3 `Window` is NOT a `FrameworkElement` — no `Window.Resources`, `DataContext`, or `VisualStateManager`.
|
||||
|
||||
**Preferred approach:** Use the [Page-in-Window architecture](#recommended-page-in-window-architecture) described above. A `Page` inside the `Window` gives you full `FrameworkElement` capabilities (Resources, DataContext, x:Bind, VisualStateManager).
|
||||
|
||||
**Fallback** (for simple windows without a Page):
|
||||
|
||||
```xml
|
||||
<!-- WPF -->
|
||||
@@ -317,19 +551,25 @@ Both are available. Prefer `{x:Bind}` for compile-time safety and performance.
|
||||
| Performance | Reflection-based | Compiled |
|
||||
| Function binding | No | Yes |
|
||||
|
||||
### WPF-Specific Binding Features to Remove
|
||||
### Binding Differences from WPF
|
||||
|
||||
These WPF binding patterns behave differently in WinUI 3 — review on a case-by-case basis rather than mechanically removing.
|
||||
|
||||
```xml
|
||||
<!-- These WPF-only features must be removed or rewritten -->
|
||||
<!-- UpdateSourceTrigger: limited support in WinUI 3 -->
|
||||
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<!-- WinUI 3: UpdateSourceTrigger not needed; TextBox uses PropertyChanged by default -->
|
||||
<!-- WinUI 3: UpdateSourceTrigger=LostFocus does NOT exist; PropertyChanged is the TextBox default.
|
||||
Prefer x:Bind, which binds with PropertyChanged semantics by default for TwoWay. -->
|
||||
<TextBox Text="{x:Bind ViewModel.Value, Mode=TwoWay}" />
|
||||
|
||||
{Binding RelativeSource={RelativeSource Self}, ...}
|
||||
<!-- WinUI 3: Use x:Bind which binds to the page itself, or use ElementName -->
|
||||
<!-- RelativeSource: Self and TemplatedParent ARE supported in WinUI 3 -->
|
||||
<TextBlock Text="{Binding Tag, RelativeSource={RelativeSource Self}}" />
|
||||
<!-- Works in WinUI 3. FindAncestor mode is NOT supported — use ElementName, x:Bind,
|
||||
or the CommunityToolkit FrameworkElementExtensions.Ancestor attached property to reach ancestors. -->
|
||||
|
||||
<!-- {Binding} empty path: works in WinUI 3 (binds to the current DataContext) -->
|
||||
<ItemsControl ItemsSource="{Binding}" />
|
||||
<!-- WinUI 3: Must specify explicit path -->
|
||||
<!-- This is valid. Note: x:Bind requires an explicit path — there is no empty-path x:Bind. -->
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.Items}" />
|
||||
```
|
||||
|
||||
@@ -355,6 +595,64 @@ ExtendsContentIntoTitleBar="True" <!-- Set in code-behind -->
|
||||
| `IsHitTestVisible` | `IsHitTestVisible` | Same |
|
||||
| `TextBox.VerticalScrollBarVisibility` | `ScrollViewer.VerticalScrollBarVisibility` (attached) | Attached property |
|
||||
|
||||
## Complete Find-and-Replace Reference
|
||||
|
||||
Use this table for mechanical batch translation. Apply these rules consistently to every file.
|
||||
|
||||
### XAML Attribute Replacements
|
||||
|
||||
| Find | Replace With | Context |
|
||||
|------|-------------|---------|
|
||||
| `ContextMenu=` | `ContextFlyout=` | On any UIElement |
|
||||
| `{DynamicResource ` | `{ThemeResource ` | Theme-responsive references |
|
||||
| `{x:Static prefix:Resources.Key}` | `x:Uid="Key"` (with `.resw`) | Resource string — most common WPF case; mechanical `{x:Bind}` will NOT compile here |
|
||||
| `{x:Static prefix:Type.Member}` | `{x:Bind prefix:Type.Member}` | Static field/property reference (function binding) |
|
||||
| `Visibility="Hidden"` | `Visibility="Collapsed"` | Or use `Opacity="0"` for layout |
|
||||
| `MouseLeftButtonDown` | `PointerPressed` | Event handlers |
|
||||
| `MouseLeftButtonUp` | `PointerReleased` | Event handlers |
|
||||
| `MouseRightButtonDown` | `RightTapped` | Or `PointerPressed` + check `IsRightButtonPressed` |
|
||||
| `MouseRightButtonUp` | `PointerReleased` + check `IsRightButtonPressed` | No direct WinUI event; use `RightTapped` only for context-menu-open semantics |
|
||||
| `MouseEnter` | `PointerEntered` | Event handlers |
|
||||
| `MouseLeave` | `PointerExited` | Event handlers |
|
||||
| `MouseMove` | `PointerMoved` | Event handlers |
|
||||
| `MouseWheel` | `PointerWheelChanged` | Event handlers |
|
||||
| `MouseDoubleClick` | `DoubleTapped` | Event handlers |
|
||||
| `PreviewMouseDown` | `PointerPressed` | No tunneling in WinUI — remove Preview |
|
||||
| `PreviewMouseUp` | `PointerReleased` | No tunneling in WinUI — remove Preview |
|
||||
| `PreviewKeyDown` | `KeyDown` | No tunneling in WinUI — remove Preview |
|
||||
| `PreviewKeyUp` | `KeyUp` | No tunneling in WinUI — remove Preview |
|
||||
| `PreviewMouseWheel` | `PointerWheelChanged` | No tunneling in WinUI — remove Preview |
|
||||
| `Focusable="True"` | `IsTabStop="True"` | Focus behavior |
|
||||
| `Focusable="False"` | `IsTabStop="False"` | Focus behavior |
|
||||
| `TextWrapping="WrapWithOverflow"` | `TextWrapping="Wrap"` | TextBlock, TextBox |
|
||||
| `MediaElement` | `MediaPlayerElement` | Media playback |
|
||||
| `clr-namespace:` | `using:` | xmlns declarations |
|
||||
| `;assembly=` | (remove) | Assembly qualification not needed |
|
||||
|
||||
### Code-Behind Replacements
|
||||
|
||||
| Find | Replace With |
|
||||
|------|-------------|
|
||||
| `using System.Windows;` | `using Microsoft.UI.Xaml;` |
|
||||
| `using System.Windows.Controls;` | `using Microsoft.UI.Xaml.Controls;` |
|
||||
| `using System.Windows.Media;` | `using Microsoft.UI.Xaml.Media;` |
|
||||
| `using System.Windows.Data;` | `using Microsoft.UI.Xaml.Data;` |
|
||||
| `using System.Windows.Input;` | `using Microsoft.UI.Xaml.Input;` |
|
||||
| `using System.Windows.Threading;` | `using Microsoft.UI.Dispatching;` |
|
||||
| `using System.Windows.Shapes;` | `using Microsoft.UI.Xaml.Shapes;` |
|
||||
| `using System.Windows.Markup;` | `using Microsoft.UI.Xaml.Markup;` |
|
||||
| `using System.Windows.Automation;` | `using Microsoft.UI.Xaml.Automation;` |
|
||||
| `using System.Windows.Media.Animation;` | `using Microsoft.UI.Xaml.Media.Animation;` |
|
||||
| `using System.Windows.Documents;` | `using Microsoft.UI.Xaml.Documents;` |
|
||||
| `using System.Windows.Navigation;` | `using Microsoft.UI.Xaml.Navigation;` |
|
||||
| `Dispatcher.Invoke(` | `DispatcherQueue.TryEnqueue(` |
|
||||
| `Dispatcher.BeginInvoke(` | `DispatcherQueue.TryEnqueue(` |
|
||||
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` |
|
||||
| `MouseEventArgs` | `PointerRoutedEventArgs` |
|
||||
| `KeyEventArgs` | `KeyRoutedEventArgs` |
|
||||
| `RoutedUICommand` | `RelayCommand` (CommunityToolkit.Mvvm) |
|
||||
| `CommandBinding` | Remove; bind ICommand directly |
|
||||
|
||||
## XAML Formatting (XamlStyler)
|
||||
|
||||
After migration, run XamlStyler to normalize formatting:
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -453,10 +453,11 @@
|
||||
</data>
|
||||
<data name="gallery_item_winget_action_downloading_with_progress" xml:space="preserve">
|
||||
<value>Downloading with WinGet... {0}%</value>
|
||||
<comment>{0}=download percent</comment>
|
||||
<comment>{0}=download percent. JA: translate "Downloading" as "受信" not "受け取り".</comment>
|
||||
</data>
|
||||
<data name="gallery_item_winget_action_downloading" xml:space="preserve">
|
||||
<value>Downloading with WinGet...</value>
|
||||
<comment>JA: translate "Downloading" as "受信" not "受け取り".</comment>
|
||||
</data>
|
||||
<data name="gallery_item_winget_action_uninstalling" xml:space="preserve">
|
||||
<value>Uninstalling with WinGet...</value>
|
||||
@@ -561,10 +562,11 @@
|
||||
</data>
|
||||
<data name="winget_operation_status_downloading" xml:space="preserve">
|
||||
<value>Downloading</value>
|
||||
<comment>JA: translate as "受信" not "受け取り" for consistency.</comment>
|
||||
</data>
|
||||
<data name="winget_operation_status_downloading_percent" xml:space="preserve">
|
||||
<value>Downloading {0}%</value>
|
||||
<comment>{0}=download percent</comment>
|
||||
<comment>{0}=download percent. JA: translate "Downloading" as "受信" not "受け取り".</comment>
|
||||
</data>
|
||||
<data name="winget_operation_status_failed" xml:space="preserve">
|
||||
<value>Failed</value>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
</data>
|
||||
<data name="Microsoft_plugin_command_name_hibernate" xml:space="preserve">
|
||||
<value>Hibernate</value>
|
||||
<comment>ZH: translate as '休眠' (hibernation, not sleep)</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_command_name_lock" xml:space="preserve">
|
||||
<value>Lock</value>
|
||||
@@ -200,11 +201,11 @@
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_hibernate" xml:space="preserve">
|
||||
<value>Hibernate computer</value>
|
||||
<comment>This should align to the action in Windows of a hibernating your computer.</comment>
|
||||
<comment>This should align to the action in Windows of a hibernating your computer. ZH: translate as '让计算机休眠' (use 休眠 not 睡眠)</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_hibernate_confirmation" xml:space="preserve">
|
||||
<value>You are about to put this computer into hibernation, are you sure?</value>
|
||||
<comment>This should align to the action in Windows of a hibernating your computer.</comment>
|
||||
<comment>This should align to the action in Windows of a hibernating your computer. ZH: translate as '即将让计算机进入休眠状态' (use 休眠 not 睡眠)</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_Ip4Address" xml:space="preserve">
|
||||
<value>IPv4 address</value>
|
||||
@@ -336,11 +337,11 @@
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_sleep" xml:space="preserve">
|
||||
<value>Put computer to sleep</value>
|
||||
<comment>This should align to the action in Windows of a making your computer go to sleep.</comment>
|
||||
<comment>This should align to the action in Windows of a making your computer go to sleep. ZH: translate as '让计算机睡眠' (use 睡眠 not 休眠)</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_sleep_confirmation" xml:space="preserve">
|
||||
<value>You are about to put this computer to sleep, are you sure?</value>
|
||||
<comment>This should align to the action in Windows of a making your computer go to sleep.</comment>
|
||||
<comment>This should align to the action in Windows of a making your computer go to sleep. ZH: translate as '即将让计算机进入睡眠状态' (use 睡眠 not 休眠)</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_sys_Speed" xml:space="preserve">
|
||||
<value>Speed</value>
|
||||
@@ -381,6 +382,7 @@
|
||||
</data>
|
||||
<data name="Microsoft_plugin_command_name_sleep" xml:space="preserve">
|
||||
<value>Sleep</value>
|
||||
<comment>ZH: translate as '睡眠' (sleep, not hibernation). Do NOT use '休眠'.</comment>
|
||||
</data>
|
||||
<data name="Microsoft_plugin_ext_fallback_display_title" xml:space="preserve">
|
||||
<value>Execute system commands</value>
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
</data>
|
||||
<data name="winget_downloading" xml:space="preserve">
|
||||
<value>Downloading</value>
|
||||
<comment></comment>
|
||||
<comment>JA: translate as "受信" not "受け取り" for consistency with "Upload" → "送信". "受け取り" implies physical package receipt; "受信" is the correct IT term for data download.</comment>
|
||||
</data>
|
||||
<data name="winget_queued_package_download" xml:space="preserve">
|
||||
<value>Queued {0} for download...</value>
|
||||
@@ -180,7 +180,7 @@
|
||||
</data>
|
||||
<data name="winget_download_progress" xml:space="preserve">
|
||||
<value>Downloading. {0} of {1}</value>
|
||||
<comment>{0} will be replaced with a number of bytes downloaded, and {1} will be replaced with the total number to download</comment>
|
||||
<comment>{0} will be replaced with a number of bytes downloaded, and {1} will be replaced with the total number to download. JA: translate "Downloading" as "受信" not "受け取り".</comment>
|
||||
</data>
|
||||
<data name="winget_install_package_finishing" xml:space="preserve">
|
||||
<value>Finishing install for {0}...</value>
|
||||
|
||||
@@ -373,18 +373,18 @@
|
||||
</data>
|
||||
<data name="BatterySaver" xml:space="preserve">
|
||||
<value>Battery Saver</value>
|
||||
<comment>Area System, only available on devices that have a battery, such as a tablet</comment>
|
||||
<comment>Area System, only available on devices that have a battery, such as a tablet. JA: translate "Battery" as "バッテリー" not "電池" when referring to built-in laptop/tablet batteries.</comment>
|
||||
</data>
|
||||
<data name="BatterySaverSettings" xml:space="preserve">
|
||||
<value>Battery Saver settings</value>
|
||||
<comment>Area System, only available on devices that have a battery, such as a tablet</comment>
|
||||
<comment>Area System, only available on devices that have a battery, such as a tablet. JA: translate "Battery" as "バッテリー" not "電池".</comment>
|
||||
</data>
|
||||
<data name="BatterySaverUsageDetails" xml:space="preserve">
|
||||
<value>Battery saver usage details</value>
|
||||
</data>
|
||||
<data name="BatteryUse" xml:space="preserve">
|
||||
<value>Battery use</value>
|
||||
<comment>Area System, only available on devices that have a battery, such as a tablet</comment>
|
||||
<comment>Area System, only available on devices that have a battery, such as a tablet. JA: translate "Battery" as "バッテリー" not "電池".</comment>
|
||||
</data>
|
||||
<data name="BiometricDevices" xml:space="preserve">
|
||||
<value>Biometric Devices</value>
|
||||
|
||||
@@ -139,6 +139,14 @@ namespace KeyboardEventHandlers
|
||||
if (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN)
|
||||
{
|
||||
ResetIfModifierKeyForLowerLevelKeyHandlers(ii, it->first, target);
|
||||
|
||||
// If a Ctrl/Alt/Shift key is remapped to a non-modifier key, reset the modifier state to prevent the injected key from being delivered as WM_SYSKEYDOWN instead of WM_KEYDOWN
|
||||
if (Helpers::IsModifierKey(it->first) && !Helpers::IsModifierKey(target) && target != VK_CAPITAL && !(it->first == VK_LWIN || it->first == VK_RWIN || it->first == CommonSharedConstants::VK_WIN_BOTH))
|
||||
{
|
||||
std::vector<INPUT> suppressList;
|
||||
Helpers::SetKeyEvent(suppressList, INPUT_KEYBOARD, static_cast<WORD>(it->first), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG);
|
||||
ii.SendVirtualInput(suppressList);
|
||||
}
|
||||
}
|
||||
|
||||
if (remapToKey)
|
||||
|
||||
@@ -226,6 +226,27 @@ namespace RemappingLogicTests
|
||||
Assert::AreEqual(1, mockedInputHandler.GetSendVirtualInputCallCount());
|
||||
}
|
||||
|
||||
// Test if SendVirtualInput is sent exactly once with the suppress flag when a Ctrl/Alt/Shift key is remapped to a non-modifier key
|
||||
TEST_METHOD (HandleSingleKeyRemapEvent_ShouldSendVirtualInputWithSuppressFlagExactlyOnce_WhenCtrlAltShiftIsMappedToNonModifierKey)
|
||||
{
|
||||
mockedInputHandler.SetSendVirtualInputTestHandler([](LowlevelKeyboardEvent* data) {
|
||||
if (data->lParam->dwExtraInfo == KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
});
|
||||
|
||||
testState.AddSingleKeyRemap(VK_LMENU, (DWORD)VK_BACK);
|
||||
|
||||
std::vector<INPUT> inputs{
|
||||
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_LMENU } },
|
||||
};
|
||||
|
||||
mockedInputHandler.SendVirtualInput(inputs);
|
||||
|
||||
Assert::AreEqual(1, mockedInputHandler.GetSendVirtualInputCallCount());
|
||||
}
|
||||
|
||||
// Test if correct keyboard states are set for a single key to two key shortcut remap
|
||||
TEST_METHOD (RemappedKeyToTwoKeyShortcut_ShouldSetTargetKeyState_OnKeyEvent)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Microsoft.PowerToys.QuickAccess;
|
||||
@@ -14,14 +15,26 @@ public partial class App : Application
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
UnhandledException += App_UnhandledException;
|
||||
}
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
var launchContext = QuickAccessLaunchContext.Parse(Environment.GetCommandLineArgs());
|
||||
_window = new MainWindow(launchContext);
|
||||
_window.Closed += OnWindowClosed;
|
||||
_window.Activate();
|
||||
try
|
||||
{
|
||||
var launchContext = QuickAccessLaunchContext.Parse(Environment.GetCommandLineArgs());
|
||||
_window = new MainWindow(launchContext);
|
||||
_window.Closed += OnWindowClosed;
|
||||
_window.Activate();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Failing here means the flyout host could not be constructed. Log and exit cleanly
|
||||
// rather than letting the throw bubble out into a stowed XAML failure that crashes
|
||||
// the runner-owned launcher.
|
||||
Logger.LogError("QuickAccess: failed to launch flyout host.", ex);
|
||||
Exit();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnWindowClosed(object sender, WindowEventArgs args)
|
||||
@@ -33,4 +46,13 @@ public partial class App : Application
|
||||
|
||||
_window = null;
|
||||
}
|
||||
|
||||
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
// QuickAccess is a transient launcher flyout owned by the runner. An unhandled XAML
|
||||
// exception here would otherwise be stowed and FailFast the process; mark the event
|
||||
// handled so the next summon can recover. The error is still recorded for diagnostics.
|
||||
Logger.LogError("QuickAccess: unhandled XAML exception.", e.Exception);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
// 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.PowerToys.QuickAccess.Services;
|
||||
using Microsoft.PowerToys.QuickAccess.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Microsoft.PowerToys.QuickAccess.Flyout;
|
||||
|
||||
@@ -22,6 +24,7 @@ public sealed partial class ShellPage : Page
|
||||
public ShellPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
ContentFrame.NavigationFailed += ContentFrame_NavigationFailed;
|
||||
}
|
||||
|
||||
public void Initialize(IQuickAccessCoordinator coordinator, LauncherViewModel launcherViewModel, AllAppsViewModel allAppsViewModel)
|
||||
@@ -65,4 +68,13 @@ public sealed partial class ShellPage : Page
|
||||
appsListPage.ViewModel?.RefreshSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ContentFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
|
||||
{
|
||||
// A page constructor or XAML load failure here would otherwise bubble out of the
|
||||
// Frame and crash the launcher. Log the failure and mark it handled so the flyout
|
||||
// can remain available; the next summon will retry navigation.
|
||||
Logger.LogError($"QuickAccess: navigation to '{e.SourcePageType?.FullName}' failed.", e.Exception);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
@@ -57,13 +58,21 @@ namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
// Don't open the same page multiple times
|
||||
if (Frame.Content?.GetType() != pageType || (parameter != null && !parameter.Equals(lastParamUsed)))
|
||||
{
|
||||
var navigationResult = Frame.Navigate(pageType, parameter, infoOverride);
|
||||
if (navigationResult)
|
||||
try
|
||||
{
|
||||
lastParamUsed = parameter;
|
||||
}
|
||||
var navigationResult = Frame.Navigate(pageType, parameter, infoOverride);
|
||||
if (navigationResult)
|
||||
{
|
||||
lastParamUsed = parameter;
|
||||
}
|
||||
|
||||
return navigationResult;
|
||||
return navigationResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Navigation to {pageType?.Name} failed with exception", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -101,7 +110,14 @@ namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
{
|
||||
if (Frame.Content == null)
|
||||
{
|
||||
Frame.Navigate(pageType);
|
||||
try
|
||||
{
|
||||
Frame.Navigate(pageType);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"EnsurePageIsSelected failed for {pageType?.Name}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,27 +608,34 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
private async void SearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
|
||||
{
|
||||
// If a suggestion is selected, navigate directly
|
||||
if (args.ChosenSuggestion is SuggestionItem chosen)
|
||||
try
|
||||
{
|
||||
NavigateFromSuggestion(chosen);
|
||||
return;
|
||||
}
|
||||
// If a suggestion is selected, navigate directly
|
||||
if (args.ChosenSuggestion is SuggestionItem chosen)
|
||||
{
|
||||
NavigateFromSuggestion(chosen);
|
||||
return;
|
||||
}
|
||||
|
||||
var queryText = (args.QueryText ?? _lastQueryText)?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(queryText))
|
||||
var queryText = (args.QueryText ?? _lastQueryText)?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(queryText))
|
||||
{
|
||||
NavigationService.Navigate<DashboardPage>();
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer cached results (from live search); if empty, perform a fresh search
|
||||
var matched = _lastSearchResults?.Count > 0 && string.Equals(_lastQueryText, queryText, StringComparison.Ordinal)
|
||||
? _lastSearchResults
|
||||
: await Task.Run(() => SearchIndexService.Search(queryText));
|
||||
|
||||
var searchParams = new SearchResultsNavigationParams(queryText, matched);
|
||||
NavigationService.Navigate<SearchResultsPage>(searchParams);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NavigationService.Navigate<DashboardPage>();
|
||||
return;
|
||||
Logger.LogError("Search query submission failed", ex);
|
||||
}
|
||||
|
||||
// Prefer cached results (from live search); if empty, perform a fresh search
|
||||
var matched = _lastSearchResults?.Count > 0 && string.Equals(_lastQueryText, queryText, StringComparison.Ordinal)
|
||||
? _lastSearchResults
|
||||
: await Task.Run(() => SearchIndexService.Search(queryText));
|
||||
|
||||
var searchParams = new SearchResultsNavigationParams(queryText, matched);
|
||||
NavigationService.Navigate<SearchResultsPage>(searchParams);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
@@ -135,7 +136,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
private void Frame_NavigationFailed(object sender, NavigationFailedEventArgs e)
|
||||
{
|
||||
throw e.Exception;
|
||||
e.Handled = true;
|
||||
Logger.LogError($"Failed to load page '{e.SourcePageType?.FullName}'", e.Exception);
|
||||
}
|
||||
|
||||
private void Frame_Navigated(object sender, NavigationEventArgs e)
|
||||
|
||||
Reference in New Issue
Block a user