Compare commits

...

18 Commits

Author SHA1 Message Date
Mike Griese
da880280be fix the BG regression on merge from main 2026-06-18 15:10:13 -05:00
Mike Griese
8d6a8e8a9e Merge remote-tracking branch 'origin/main' into dev/migrie/compact-cmdpal-2 2026-06-18 09:22:19 -05:00
ABHIJEET KALE
8dc4598786 [CmdPal][System] Add Chinese (ZH) translation guidance for Sleep vs Hibernate (#48534) (#48653)
## Summary

Fixes incorrect Chinese translation where both Sleep and Hibernate
commands showed '休眠' (hibernation) instead of using distinct terms: '睡眠'
for Sleep and '休眠' for Hibernate.

## Changes

Added \<comment>\ elements with \ZH:\ guidance to 6 resource entries in
\Microsoft.CmdPal.Ext.System\\Properties\\Resources.resx\:

| Resource Key | Current Comment | Added ZH Guidance |
|---|---|---|
| \Microsoft_plugin_command_name_hibernate\ | *(none)* | translate as
'休眠' |
| \Microsoft_plugin_sys_hibernate\ | Existing English comment | use 休眠
not 睡眠 |
| \Microsoft_plugin_sys_hibernate_confirmation\ | Existing English
comment | use 休眠 not 睡眠 |
| \Microsoft_plugin_command_name_sleep\ | *(none)* | translate as '睡眠',
do NOT use '休眠' |
| \Microsoft_plugin_sys_sleep\ | Existing English comment | use 睡眠 not
休眠 |
| \Microsoft_plugin_sys_sleep_confirmation\ | Existing English comment |
use 睡眠 not 休眠 |

## Why comments only?

Chinese translations are managed by the internal CDPX localization
pipeline. Adding \<comment>\ elements to the English \.resx\ file is the
standard way to guide translators for the next localization pass. See
similar fix in PR #48649 for Japanese translation guidance.
2026-06-18 12:26:32 +02:00
ABHIJEET KALE
fa8fbb60a1 [CmdPal][Localization] Add JA translation guidance comments for Download and Battery strings (#48649)
## Summary

Adds translation guidance comments to English .resx resource strings
that have incorrect Japanese translations in the CDPX localization
pipeline.

## Problem

- **Download/Downloading** — translated as 受け取り (implies physical
package receipt) instead of the correct IT term 受信
- **Battery** (laptop/tablet context) — translated as 電池 (dry cell)
instead of バッテリー (built-in battery)

## Fix

Added comment elements to 9 resource entries across 3 files with the
prefix JA: to guide the localization team:

| File | Strings | Comment |
|------|---------|---------|
| Microsoft.CmdPal.Ext.WinGet\Properties\Resources.resx |
winget_downloading, winget_download_progress | JA: translate as 受信 not
受け取り |
| Microsoft.CmdPal.UI.ViewModels\Properties\Resources.resx |
winget_operation_status_downloading,
winget_operation_status_downloading_percent,
gallery_item_winget_action_downloading_with_progress,
gallery_item_winget_action_downloading | JA: translate as 受信 not 受け取り |
| Microsoft.CmdPal.Ext.WindowsSettings\Properties\Resources.resx |
BatterySaver, BatterySaverSettings, BatteryUse | JA: translate as バッテリー
not 電池 |

## Background

As documented in doc/devdocs/development/localization.md, localized
.resx files are generated at build time by the CDPX pipeline from .lcl
files managed by the internal localization team. The comment elements in
English .resx files are surfaced to translators and are the correct
mechanism for providing translation guidance. The actual .lcl file fixes
must be applied by the internal team.

Closes #48598
2026-06-18 12:25:40 +02:00
Clint Rutkas
ab4947579b [QuickAccess] Suppress unhandled XAML exceptions in flyout host (#48457)
## Summary

Adds the two missing top-level exception handlers in the QuickAccess
(Preview) flyout host so that an unhandled XAML exception during launch
or page navigation no longer FailFasts `PowerToys.QuickAccess.exe`.

Spotted while reading through `App.OnLaunched` and `ShellPage` for an
unrelated review of the flyout startup path — none of the existing
handlers exist yet, so any throw during `MainWindow` construction,
`ShellHost.Initialize`, or `ContentFrame.Navigate(typeof(LaunchPage) |
typeof(AppsListPage), …)` bubbles all the way out to the Windows App SDK
runtime and is stowed as a XAML failure. Compare with
`src\settings-ui\Settings.UI\SettingsXAML\App.xaml.cs`, which already
wires `UnhandledException += App_UnhandledException`.

## Changes

**`src\settings-ui\QuickAccess.UI\QuickAccessXAML\App.xaml.cs`**

- Hook `Application.UnhandledException` in the constructor. The handler
logs the exception via `ManagedCommon.Logger.LogError` (same logger
Settings uses) and sets `e.Handled = true`. QuickAccess is a transient
launcher flyout owned by the runner, so swallowing a stray XAML error
and keeping the host alive for the next summon is the correct trade-off
— the failure is still recorded for diagnostics.
- Wrap the body of `OnLaunched` in a try/catch. If `MainWindow` (which
sets up window chrome, listener threads, the IPC coordinator, and the
XAML shell) fails to construct, log the exception and call `Exit()`
cleanly rather than letting the throw escape into the Windows App SDK
launch path.


**`src\settings-ui\QuickAccess.UI\QuickAccessXAML\Flyout\ShellPage.xaml.cs`**

- Subscribe to `ContentFrame.NavigationFailed` after
`InitializeComponent`. A page constructor or XAML-load failure in
`LaunchPage` / `AppsListPage` would otherwise bubble out of the `Frame`
and crash the launcher. The handler logs the failure
(`SourcePageType.FullName` + the exception) and marks it handled so the
next summon retries navigation.

No production behaviour changes when things work — only the failure
paths are different. No public API surface changes.

## Why both handlers, not just one

- `Application.UnhandledException` does not fire for
`Frame.NavigationFailed`. The Frame raises its own event first and, if
no handler runs or `e.Handled` is left `false`, then it rethrows on the
dispatcher.
- Conversely, `Frame.NavigationFailed` only fires for navigation
failures — not for an exception thrown directly in `OnLaunched` before
any navigation happens.

The two events are complementary, so both need a handler to fully cover
the launch + navigation paths.

## Testing

- The local NuGet feed on my dev box currently can't restore
`Microsoft.NETCore.App.Runtime.win-x64 = 10.0.9` (the feed only has
`11.0.0-preview.1.26104.118`), which fails the project restore for every
WinUI project including this one. That's the same environment issue I
called out on #48414 — pipeline restore uses a different feed and is
fine.
- All three patterns added here are copy-paste analogues of code that
already exists in `Settings.UI` (`App.xaml.cs:96, 106-109`,
`ShellViewModel.cs:86, 136`), so namespace and signature drift risk is
minimal. The only behavioural difference is `e.Handled = true`, which is
the actual goal of this PR.

## Risk

- Low. Two new event handlers and one try/catch. No behaviour change on
the success path.
- Worst-case regression is that a real, repeatable XAML failure becomes
silent in the runner's eyes (no process crash) instead of loud — but
it's logged via `Logger.LogError` so the user can still find the trace
in `%LOCALAPPDATA%\Microsoft\PowerToys\Logs\`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---

ADO:
https://microsoft.visualstudio.com/DefaultCollection/OS/_workitems/edit/61258633/

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 12:20:38 +02:00
moooyo
d221f84d8f [Skills] Update WinUI3 migration skills to add more migration mapping item (#47043)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #46464
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-18 12:17:49 +02:00
Tucker Burns
14152672c5 Add defensive error handling for Settings navigation and search (#46688)
## Summary

Addresses the **root cause chain** behind the NullReferenceException
crash in Settings navigation. Crash dump analysis with WinDbg revealed
that `Frame.Navigate()` can throw native WinUI ABI exceptions that
propagate unhandled through multiple code paths.

### Changes (3 files)

**NavigationService.cs** — Defensive try-catch for all
`Frame.Navigate()` call sites:
- `Navigate()`: Wrap in try-catch, log via `Logger.LogError()`, return
`false` on failure
- `EnsurePageIsSelected()`: Add matching try-catch for consistency (also
calls `Frame.Navigate()` unprotected)
- Protects **all** navigation in the Settings app

**ShellViewModel.cs** — Fix the crash-causing `Frame_NavigationFailed`
handler:
- Replace `throw e.Exception` with `e.Handled = true` +
`Logger.LogError()`
- The original code threw `NullReferenceException` when `e.Exception`
was null (native WinUI errors don't always marshal to managed Exception
objects)
- Mark the event as handled to prevent framework error propagation

**ShellPage.xaml.cs** — Protect `async void SearchBox_QuerySubmitted`:
- Wrap entire method body in try-catch
- `async void` methods that throw after `await` produce unhandled
exceptions that crash the process
- Covers both search indexing (`Task.Run`) and navigation failures

### Crash Dump Evidence

Analysis of `PowerToys.Settings_2025_03_26_48636.dmp` with WinDbg:
- 4 stowed exception records found at `0x000001c60d3b9100`
- Exception chain: `SearchBox_QuerySubmitted` → `Frame.Navigate()` fails
at native ABI → `NavigationFailed` fires with null Exception → `throw
null` → `NullReferenceException` → `FailFastWithStowedExceptions`
- Root failure in `Microsoft.UI.Xaml.dll!DirectUI::Frame::Navigate`

### Review Notes
- `catch (Exception)` pattern matches 92.5% of Settings.UI codebase
convention
- `Logger.LogError()` with `using ManagedCommon` matches standard import
pattern
- No security concerns: logged page type names are not sensitive data
- Build verified locally (38/38 applicable tests pass; 111 COM-dependent
tests fail identically on main)

Co-authored-by: Tucker Burns <tuck.burns@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 11:15:52 +02:00
Matheus Mol
5688441127 [KBM] Fix modifier key remapped to non-modifier delivering WM_SYSKEYDOWN (#47192)
## Summary of the Pull Request

When a modifier key (Ctrl/Alt/Shift) is remapped to a non-modifier key
using
Keyboard Manager, the injected key event is delivered to applications as
WM_SYSKEYDOWN instead of WM_KEYDOWN. This causes unexpected behavior —
for
example, remapping Left Alt to Backspace results in whole words being
deleted
instead of single characters, because applications interpret
WM_SYSKEYDOWN +
VK_BACK as Alt+Backspace.

The fix resets the modifier state with a suppress-flag key-up event
before
injecting the target key, consistent with the existing approach used for
the
Caps Lock remapping scenario.

## PR Checklist

- [ ] Closes: #47191
- [ ] Communication: I've discussed this with core contributors already.
If the work hasn't been agreed, this work might be rejected
- [x] Tests: Added/updated and all pass
- [ ] Localization: All end-user-facing strings can be localized
- [ ] Dev docs: Added/updated
- [ ] New binaries: Added on the required places

## Detailed Description of the Pull Request / Additional comments

The root cause is that SendInput is called inside the low-level keyboard
hook
callback before the original modifier event is suppressed. At that point
the
modifier state is still active, so the OS delivers the injected key as
WM_SYSKEYDOWN (system key with Alt context) rather than WM_KEYDOWN.

This is the same mechanism that was already fixed for the Ctrl/Alt/Shift
↔
Caps Lock case. This PR extends the fix to cover modifier → non-modifier
remaps.

## Validation Steps Performed

1. Remapped Left Alt → Backspace in Keyboard Manager
2. Opened a text editor, typed text, pressed the remapped key
3. Confirmed single characters are deleted instead of whole words
4. All 93 unit tests pass (KeyboardManager.Engine.UnitTests)
2026-06-18 16:33:56 +08:00
Mike Griese
9f82e018f6 gotta do the clipping for clickthrough 2026-06-02 14:09:59 -05:00
Mike Griese
0c2bae21d4 always expand for dock pages 2026-06-02 10:22:46 -05:00
Mike Griese
c914bdf509 settings for position; fix a select-on-expand 2026-06-02 10:17:39 -05:00
Mike Griese
3851e06f45 better cold launch border 2026-06-02 05:49:18 -05:00
Mike Griese
b27ca5a9bf fuckin amazing, center the textbox vertically 2026-06-01 16:37:35 -05:00
Mike Griese
63701c9b91 holy oneshot batman 2026-06-01 16:09:42 -05:00
Mike Griese
cbb5e985c9 shit I think this works 2026-06-01 15:20:41 -05:00
Mike Griese
017e3278d9 resizing is working better 2026-06-01 13:56:35 -05:00
Mike Griese
e834031c7f Add a setting to show/hide the frame, for debugging 2026-06-01 11:01:40 -05:00
Mike Griese
981c1cefba What if the window was a smaller control inside of our HWND 2026-06-01 10:33:23 -05:00
35 changed files with 2019 additions and 318 deletions

View File

@@ -677,6 +677,7 @@ HDEVNOTIFY
hdr
HDROP
hdwwiz
Headered
Helpline
helptext
hgdiobj

View File

@@ -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. |

View File

@@ -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>/`

View File

@@ -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:

View File

@@ -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);
}

View File

@@ -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)
{
}

View File

@@ -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>

View File

@@ -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
///////////////////////////////////////////////////////////////////////////

View File

@@ -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;

View File

@@ -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(

View File

@@ -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>

View File

@@ -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,
};
}
}
}

View File

@@ -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,
};
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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)

View File

@@ -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=&#xE73F;}">
<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=&#xE74A;}"
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}" />

View File

@@ -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=&#xE737;}">
<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" />

View File

@@ -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 });
}
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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)
{

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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()

View File

@@ -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)