Compare commits

...

80 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
018f9417a1 feat(cmdpal): Implement CommandResult.GoToPage()
Add handling for CommandResultKind.GoToPage in the shell, allowing
extensions to navigate to a specific top-level page after a command
completes, with Push, GoBack, or GoHome navigation modes.

Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/dfde3ba3-e879-4fd7-ad6d-667e5ba79632
2026-03-24 12:14:45 +00:00
copilot-swe-agent[bot]
658ddead43 Initial plan 2026-03-24 11:57:01 +00:00
moooyo
7051b8939b [Skills] Add WPF to WinUI 3 migration agent skill (#46462)
<!-- 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
With this skills, we can easily enable AI to complete most of the tasks
involved in migrating from WPF to WinUI3.

<!-- 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 <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:40:33 +00:00
moooyo
0d41d45a64 [Settings] Decouple Settings.UI.Library from PowerDisplay.Lib to fix … (#46325)
<!-- 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
Fixes PowerToys Run crash (`FileNotFoundException` for
`PowerDisplay.Lib.dll`) caused by `Settings.UI.Library` having a
transitive dependency on `PowerDisplay.Lib`.

`SettingsSerializationContext` registered PowerDisplay profile types
(`PowerDisplayProfile`, `PowerDisplayProfiles`, `ProfileMonitorSetting`)
via `[JsonSerializable]` attributes, which forced the CLR to load
`PowerDisplay.Lib.dll` at startup. PowerToys Run depends on
`Settings.UI.Library` but does not ship `PowerDisplay.Lib.dll`, causing
the crash.



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

- [x] Closes: #46261
<!-- - [ ] 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-03-24 07:06:07 +00:00
Michael Jolley
86115a54f6 CmdPal: Extract persistence services from SettingsModel and AppStateModel (#46312)
## Summary of the Pull Request

Extracts persistence (load/save) logic from `SettingsModel` and
`AppStateModel` into dedicated service classes, following the
single-responsibility principle. Consumers now interact with
`ISettingsService` and `IAppStateService` instead of receiving raw model
objects through DI.

**New services introduced:**
- `IPersistenceService` / `PersistenceService` — generic `Load<T>` /
`Save<T>` with AOT-compatible `JsonTypeInfo<T>`, ensures target
directory exists before writing
- `ISettingsService` / `SettingsService` — loads settings on
construction, runs migrations, exposes `Settings` property and
`SettingsChanged` event
- `IAppStateService` / `AppStateService` — loads state on construction,
exposes `State` property and `StateChanged` event

**Key changes:**
- `SettingsModel` and `AppStateModel` are now pure data models — all
file I/O, migration, and directory management removed
- Raw `SettingsModel` and `AppStateModel` removed from DI container;
consumers receive the appropriate service
- `IApplicationInfoService.ConfigDirectory` injected into services for
config path resolution (no more hardcoded `Utilities.BaseSettingsPath`)
- ~30 consumer files updated across `Microsoft.CmdPal.UI.ViewModels` and
`Microsoft.CmdPal.UI` projects
- All `#pragma warning disable SA1300` suppressions removed —
convenience accessors replaced with direct `_settingsService.Settings` /
`_appStateService.State` access
- Namespace prefixes (`Services.ISettingsService`) replaced with proper
`using` directives

## PR Checklist

- [ ] **Communication:** I've discussed this with core contributors
already.
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** N/A — no end-user-facing strings changed
- [ ] **Dev docs:** N/A — internal refactor, no public API changes
- [ ] **New binaries:** N/A — no new binaries introduced

## Detailed Description of the Pull Request / Additional comments

### Architecture

Services are registered as singletons in `App.xaml.cs`:
```csharp
services.AddSingleton<IPersistenceService, PersistenceService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IAppStateService, AppStateService>();
```

`PersistenceService.Save<T>` writes the serialized model directly to
disk, creating the target directory if it doesn't exist. It also does
not attempt to merge existing and new settings/state. `SettingsService`
runs hotkey migrations on load and raises `SettingsChanged` after saves.
`AppStateService` always raises `StateChanged` after saves.

### Files changed (41 files, +1169/−660)

| Area | Files | What changed |
|------|-------|-------------|
| New services | `Services/IPersistenceService.cs`,
`PersistenceService.cs`, `ISettingsService.cs`, `SettingsService.cs`,
`IAppStateService.cs`, `AppStateService.cs` | New service interfaces and
implementations |
| Models | `SettingsModel.cs`, `AppStateModel.cs` | Stripped to pure
data bags |
| DI | `App.xaml.cs` | Service registration, removed raw model DI |
| ViewModels | 12 files | Constructor injection of services |
| UI | 10 files | Service injection replacing model access |
| Settings | `DockSettings.cs` | `Colors.Transparent` replaced with
struct literal to avoid WinUI3 COM dependency |
| Tests | `PersistenceServiceTests.cs`, `SettingsServiceTests.cs`,
`AppStateServiceTests.cs` | 38 unit tests covering all three services |
| Config | `.gitignore` | Added `.squad/`, `.github/agents/` exclusions
|

## Validation Steps Performed

- Built `Microsoft.CmdPal.UI` with MSBuild (x64/Debug) — exit code 0,
clean build
- Ran 38 unit tests via `vstest.console.exe` — all passing
- Verified no remaining `#pragma warning disable SA1300` blocks
- Verified no remaining `Services.` namespace prefixes

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 18:58:27 -05:00
Kai Tao
99706d4324 PowerToys Extension: Fancyzone layout command should be able to be pinned into dock (#46198)
<!-- 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
Currently, the layout do not have id, they can't be pinned into dock
<img width="1182" height="711" alt="image"
src="https://github.com/user-attachments/assets/67461267-4bed-4c07-99ff-7311c368ad09"
/>

This pr address this and they can be pinned into dock

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

- [ ] Closes: #xxx
<!-- - [ ] 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


https://github.com/user-attachments/assets/8e7c8b04-663d-4cd3-b26f-d74e46511feb

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-19 13:31:10 +08:00
Jessica Dene Earley-Cha
ff194c0b5f [Cmdpal] Screen reader announces "Enter Alias" instead of "Alias" in ExtensionPage (#45906)
<!-- 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
Fixes a screen reader accessibility issue where the Alias TextBox in the
ExtensionPage settings was announcing "Enter Alias" as its accessible
name instead of just "Alias", making it confusing for users relying on
assistive technology.

This is part of the a11y bug batch.
User Impact:
Duplicate announcements can confuse users, increase cognitive load, and
slow down interaction for users relying on assistive technologies.

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

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **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



https://github.com/user-attachments/assets/3a676226-0b07-4e60-b2d8-99ae46ff5b64
2026-03-18 11:23:32 -07:00
Jessica Dene Earley-Cha
a151d6c8b6 [cmdpal] Announce Shortcut key information (#46164)
<!-- 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
Add audio text about shortcut key for settings on the settings button

This is part of the a11y bug batch.
User Impact:
Users relying on assistive technologies may be unaware of shortcut keys,
reducing efficiency and making it harder to use the Command Palette
effectively.

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

- [ ] Closes: #xxx
<!-- - [ ] 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



https://github.com/user-attachments/assets/5a5f16dc-52f7-4e9d-85db-173eddba5d6d

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2026-03-18 11:22:49 -07:00
Niels Laute
7610b77109 [Run] Remove unused namespaces (#46221)
<!-- 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

Related: #46220

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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-18 19:16:56 +01:00
moooyo
549b32e273 [Security][Hosts] Remove open method lookup and hardcode the notepad to open host editor (#46194)
<!-- 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
**This is a security issue fix**

In Hosts Editor module, we support to open the hosts file through user's
default editor.
The way to find the editor is that we will read a registry key and use
the value as a parameter to call Process.Start.

In most case, it works well. But Hosts editor module will launch as
admin permission. But the registry value can be modified in user 's
permission (without UAC request).

So, the malware may change the registry value and if user click the open
button. May call the dangerous command which written in registry.


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

- [x] Closes: #46195
<!-- - [ ] 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>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 13:35:45 +08:00
Niels Laute
0ccf5986e9 0.98 release update for ReadMe (#46180)
<!-- 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

- [ ] Closes: #xxx
<!-- - [ ] 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: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
2026-03-17 12:35:31 -05:00
Kai Tao
87b24afa23 Security: Fix Local privilege escalation via DLL hijack (#46145)
<!-- 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
Attack vector:
1. user install per machine installer
2. Open an elevated command prompt and verify the newly added PowerToys
PATH entry
3. Inspect the ACL on the DSCModules directory an observe that the
"Authenticated Users" group have inherited Modify permissions
4. Log in as a low-privileged (non-admin) user and confirm that you can
create or modify files in C:\\PowerToys\\DSCModules\. This confirms that
a non-admin user can plant arbitrary DLLs in a system PATH directory.
5. The attacker identifies a DLL that a privileged process (e.g., a
system service or an application running as a different,
higher-privileged user) attempts to load via the standard DLL search
order. The attacker crafts a malicious DLL with the same name and places
it in C:\\PowerToys\\DSCModules.

The fix is to:
* Hardening the PowerToys DSC directory for per-machine custom installs
with correct ACL enforced with wix.

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

- [ ] Closes: #xxx
<!-- - [ ] 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

<img width="836" height="449" alt="image"
src="https://github.com/user-attachments/assets/f21a814c-6514-4a86-b214-0984653aaab4"
/>


After upgrade, the ACL:

Path : Microsoft.PowerShell.Core\FileSystem::C:\apps\Power
Toys\DSCModules
Owner  : NT AUTHORITY\SYSTEM
Group  : NT AUTHORITY\SYSTEM
Access : CREATOR OWNER Allow  268435456
         NT AUTHORITY\SYSTEM Allow  FullControl
         BUILTIN\Administrators Allow  FullControl
         BUILTIN\Users Allow  ReadAndExecute, Synchronize
Audit  :
Sddl :
O:SYG:SYD:P(A;OICIIO;GA;;;CO)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;0x1200a9;;;BU)

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 11:27:57 +08:00
Niels Laute
74c53c14e6 KBM Icon fix (#46157)
<!-- 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

- [ ] Closes: #xxx
<!-- - [ ] 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: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
2026-03-16 21:29:29 -04:00
Michael Jolley
77173cd075 CmdPal: Stop dock window resizes from saving for normal window opens (#46118)
Currently, if you resize a window opened from the dock (i.e. performance
monitor commands) then exit CmdPal, the resized "size" persists on
normal hotkey opens. This change tells CmdPal to revert the size when
opened and only save the size on normal window close.

Fixes #45591

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2026-03-16 19:47:54 +01:00
Niels Laute
149e7b1efe Update LightSwitchPage (#46160)
<!-- 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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-16 18:20:50 +00:00
Kai Tao
0c2d24c3f6 PowerToys Extension: Use project build because we don't need packagereference (#46080)
<!-- 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
At first version, tried to use the project reference instead of package
reference, while it did not work in my local environment, so used
package reference instead.

While planned to project reference for many reasons like first day
problem explosure in sdk, maintain the strict consistent winmd with the
extension host.

Hopefully to solve some of the extension not starting problem.

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

- [ ] Closes: #xxx
<!-- - [ ] 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

Built an installer and validated in my arm64 and x64 machine, works
perfectly
2026-03-16 10:58:21 +08:00
Michael Jolley
b81ea23c68 CmdPal: Adding a lock around perf monitor updates (#46061)
Based on reported bug in Teams.

Added lock around OnLoadBasePage._loadCount and modified
PerformanceWidgetPage to use Interlocked.Increment/Decrement on
_loadCount.

---------

Co-authored-by: leileizhang <leilzh@microsoft.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com>
Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Kai Tao <69313318+vanzue@users.noreply.github.com>
Co-authored-by: Jiří Polášek <me@jiripolasek.com>
Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
Co-authored-by: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com>
Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>
Co-authored-by: Mike Hall <mikehall@microsoft.com>
Co-authored-by: vanzue <vanzue@outlook.com>
Co-authored-by: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com>
Co-authored-by: Thanh Nguyen <74597207+ThanhNguyxn@users.noreply.github.com>
Co-authored-by: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
2026-03-11 17:06:44 -04:00
Jaylyn Barbee
39bbf0593e [KBM] Fixing text replacement bug (#46069)
There is a bug now where text replacement is causing the app to crash.
Wrapping those blocks in try catch so the worker stays alive.
2026-03-11 14:02:46 -04:00
Dave Rayment
4620f6f381 [Docs] Update PowerToys download links to version 0.97.2 (#46058)
<!-- 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
Update 0.97.1 download links to 0.97.2 in the main README.md.

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

- [x] Closes: #46057
<!-- - [ ] 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

Updated all download section descriptions and all links under the "items
that need to be updated release to release" comment, which updates the
download section links themselves.

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

Tested all download links to confirm they have been updated correctly:

<img width="418" height="392" alt="image"
src="https://github.com/user-attachments/assets/09628863-7681-4e71-9d31-bc9bb4bb91c1"
/>
2026-03-11 10:20:49 +00:00
Kai Tao
da3b12d536 Pipeline: Pipeline failed due to restore fail, fix it (#46062)
<!-- 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
As title
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-11 03:28:43 +00:00
Niels Laute
bab77edd11 [Dock] Add pin instructions (#46052)
## Summary of the Pull Request

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

<img width="549" height="341" alt="image"
src="https://github.com/user-attachments/assets/e09e3686-1ffd-4929-8828-061a4aa42fbb"
/>

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-10 22:37:51 -04:00
Jiří Polášek
414ee86fb3 CmdPal: make RDP fallback non-global by default (#46053)
## Summary of the Pull Request

This PR makes RDP fallback non-global by default.

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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-10 22:37:01 -04:00
Gordon Lam
eeeb6c0c93 feat(winmd-api-search): add WinMD cache generator script and related files (#45606)
<!-- 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

Adds a new Copilot agent skill (`winmd-api-search`) that lets AI agents
discover and explore Windows desktop APIs by searching a local cache of
WinMD metadata. The skill covers Windows Platform SDK, WinAppSDK/WinUI,
NuGet package WinMDs, and project-output WinMDs — providing full API
surface details (types, members, enumeration values, namespaces) without
needing external documentation lookups.

**Key components:**

- `.github/skills/winmd-api-search/SKILL.md` — Skill definition with
usage instructions, search/detail workflows, and scoring guidance
- `scripts/Invoke-WinMdQuery.ps1` — PowerShell query engine supporting
actions: `search`, `type`, `members`, `enums`, `namespaces`, `stats`,
`projects`
- `scripts/Update-WinMdCache.ps1` — Orchestrator that builds the C#
cache generator, discovers project files, and runs the generator
- `scripts/cache-generator/CacheGenerator.csproj` + `Program.cs` — .NET
console app using `System.Reflection.Metadata` to parse WinMD files from
NuGet packages, project references, Windows SDK, and packages.config
into per-package JSON caches
- `scripts/cache-generator/Directory.Build.props`,
`Directory.Build.targets`, `Directory.Packages.props` — Empty isolation
files to prevent repo-level Central Package Management and build targets
from interfering with this standalone tool

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

- [ ] Closes: #xxx <!-- Replace with issue number if applicable -->
- [x] **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 — N/A: This is an offline
agent skill (PowerShell + standalone .NET tool) with no integration into
the main product build or runtime. Validated manually by running the
cache generator across multiple project contexts (ColorPickerUI,
CmdPal.UI, runner, ImageResizer, etc.) and exercising all query actions.
- [ ] **Localization:** All end-user-facing strings can be localized —
N/A: No end-user-facing strings; this is an internal developer/agent
tool
- [ ] **Dev docs:** Added/updated — The SKILL.md itself serves as the
documentation
- [ ] **New binaries:** Added on the required places — N/A: The cache
generator is a standalone dev-time tool, not shipped in the installer
- [ ] **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: N/A

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

### Cache Generator (`Program.cs`, ~1000 lines)

A self-contained .NET console app that:

1. **Discovers WinMD sources** from four channels:
   - `project.assets.json` (PackageReference — modern .csproj/.vcxproj)
   - `packages.config` (legacy NuGet format)
- `<ProjectReference>` bin/ output (class libraries producing `.winmd`)
   - Windows SDK `UnionMetadata/` (highest installed version)

2. **Parses WinMD files** using `System.Reflection.Metadata` /
`PEReader` to extract:
- Types (classes, structs, interfaces, enums, delegates) with full
namespace
- Members (methods with decoded signatures/parameters, properties with
accessors, events)
   - Enum values
   - Base types and type kinds

3. **Outputs per-package JSON** under `Generated Files/winmd-cache/`:
- `packages/<Id>/<Version>/meta.json` — package summary
(type/member/namespace counts)
   - `packages/<Id>/<Version>/namespaces.json` — ordered namespace list
- `packages/<Id>/<Version>/types/<Namespace>.json` — full type detail
per namespace
- `projects/<ProjectName>.json` — maps each project to its package set

4. **Deduplicates** at the package level — if a package+version is
already cached, it's skipped on subsequent runs.

### Build Isolation

Three empty MSBuild files (`Directory.Build.props`,
`Directory.Build.targets`, `Directory.Packages.props`) in the
cache-generator folder prevent the repo's Central Package Management and
shared build configuration from interfering with this standalone tool.

### Query Engine (`Invoke-WinMdQuery.ps1`)

Supports seven actions: `search` (fuzzy text search across
types/members), `type` (full detail for a specific type), `members`
(filtered members of a type), `enums` (enumeration values), `namespaces`
(list all namespaces), `stats` (cache statistics), and `projects` (list
cached projects with their packages).

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

1. **Cache generation:** Ran `Update-WinMdCache.ps1` across 310+ project
files in the repo — 8 packages parsed, 316 reused from cache, all
completed without errors
2. **Query testing on multiple projects:**
   - `ColorPickerUI` — verified Windows SDK baseline (7,023 types)
- `Microsoft.CmdPal.UI.ViewModels` (after restore) — verified 13
packages, 49,799 types, 112,131 members including WinAppSDK,
AdaptiveCards, CsWinRT, Win32Metadata
   - `runner` (C++ vcxproj) — verified packages.config fallback path
   - `ImageResizerExt` — verified project reference WinMD discovery
3. **All seven query actions validated:** `stats`, `search`,
`namespaces`, `type`, `enums`, `members`, `projects` — all returned
correct results
4. **Spell-check compliance:** SKILL.md vocabulary reviewed against
repo's check-spelling dictionaries; replaced flagged words with standard
alternatives

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-11 10:15:32 +08:00
Jaylyn Barbee
70e082ce4f [Keyboard Manager] Replace text update (#46046)
<!-- 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
Doing multiple text replacements in a row was unresponsive and laggy.
This PR addresses that issue.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [x] Closes: #46031 

## Validation steps
- Tested before according to the above issue, observed the bug
- Tested and refined the experience manually both locally and with the
exe from the CI build
2026-03-10 21:31:14 -04:00
Niels Laute
8404bfbebb CmdPal-New extension - Show error when path does not exist (#46037)
<!-- 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

If path does not exist, error message is shown vs nothing

<img width="1194" height="899" alt="image"
src="https://github.com/user-attachments/assets/ee40e49c-185c-418a-9815-1ad46d976a0e"
/>


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

- [x] Closes: #38320
<!-- - [ ] 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
2026-03-10 15:07:39 -04:00
Jiří Polášek
77412d1961 Settings: Add a solution filter for PowerToys Settings UI and related projects (#46036)
## Summary of the Pull Request

This PR adds a solution filter for projects related to PowerToys
Settings

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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-10 15:05:28 -04:00
Niels Laute
fad5a3ac69 Adding NEW tag to KBM and Dock (#46048)
<!-- 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

<img width="325" height="206" alt="image"
src="https://github.com/user-attachments/assets/aab57a42-747d-4437-9326-2b9cfcdc8b80"
/>


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

- [] Closes: #xxx
<!-- - [ ] 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
2026-03-10 14:59:48 -04:00
Niels Laute
52cab7058a [CmdPal DetailsView] Resetting scroll upon selection (#46038)
<!-- 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

To test:
- Open WinGet, search for PowerToys
- Select an option, scroll down in the DetailsPage
- Use the arrow code to select up / down

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

- [x] Closes: #38334
- [ ] **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
2026-03-10 19:47:51 +01:00
Niels Laute
35a3c55f29 [CmdPal] Minor string tweaks (#46040)
<!-- 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

Before:
<img width="1198" height="718" alt="image"
src="https://github.com/user-attachments/assets/79a23cb3-93c6-4960-afc5-60c147f8ae92"
/>

After:
<img width="1175" height="716" alt="image"
src="https://github.com/user-attachments/assets/c4ea1e2d-ad47-4203-a77a-99f22bcfb448"
/>

<img width="630" height="434" alt="image"
src="https://github.com/user-attachments/assets/8ad1d424-2907-4599-965d-1193effaf7bd"
/>

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

- [x] Closes: #38730 (a lot of the suggestions there were already
resolved.
<!-- - [ ] 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: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-03-10 19:47:38 +01:00
Kai Tao
ed16ae7b2a Zoomit: Fix a issue that after trim, the video can't be saved and we can't start a new recording session (#46034)
<!-- 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

The bug was caused by a resource lifetime issue between the recording
phase and the save/trim phase. After a recording stopped,
StartRecordingAsync moved directly into the save workflow while it was
still holding the temporary recording stream and the active recording
session objects, later file operations in the save flow could fail
against that same file. Once that happened, ZoomIt could end up stuck in
a bad state where the first save did not complete cleanly and subsequent
recording attempts would no longer start until ZoomIt was restarted.



<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
As title
- [ ] Closes: #46006
<!-- - [ ] 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
Validated locally recording works fine when trimmed
2026-03-10 16:41:15 +01:00
Niels Laute
f049cc5839 Setting MinWidth on Shortcut user control (#46035)
Closes: #45683
2026-03-10 10:37:09 +00:00
Kai Tao
f82fb2a411 Newplus: Change the built-in newplus status will trigger error (#46029)
<!-- 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
Fix #46026
<!-- Please review the items on the PR checklist before submitting-->

## PR Checklist

- [ ] Closes: #46026
<!-- - [ ] 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



https://github.com/user-attachments/assets/9150c15e-6478-46f2-92fa-771cdcc0ad01
2026-03-10 16:58:46 +08:00
Jiří Polášek
90131e35d9 CmdPal: Prevent unsynchornized access to More commands (#46020)
## Summary of the Pull Request

This PR fixes a crash caused by unsynchronized access to a context menu:

- Fixes `System.ArgumentException: Destination array was not long
enough` crash in `CommandItemViewModel.AllCommands` caused by
`List<T>.AddRange()` racing with background `BuildAndInitMoreCommands`
mutations
- Replaces mutable `List<T>` public surfaces with immutable array
snapshots protected by a `Lock`; writers hold the lock, mutate the
backing list, then atomically publish new snapshots via `volatile`
fields that readers access lock-free
- Applies the same snapshot pattern to `ContentPageViewModel`, using a
bundled `CommandSnapshot` object for atomic publication (since
`PrimaryCommand` drives command invocation there, not just UI hints)
- Narrows `IContextMenuContext.MoreCommands` and `AllCommands` from
`List<T>`/`IEnumerable<T>` to `IReadOnlyList<T>` to prevent consumers
from casting back and mutating
- Moves `SafeCleanup()` calls outside locks in cleanup paths to avoid
holding the lock during cross-process RPC calls

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

- [x] Closes: #45975 
<!-- - [ ] 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
2026-03-09 17:34:44 -05:00
Jiří Polášek
77355ef2fb CmdPal: change visibility of a search box before bailing out (#46021)
## Summary of the Pull Request

This PR ensures the search box visibility is correctly set by moving the
assignment before the short-circuit bail-out when the window is no
longer visible.

Regressed in 4959273

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

- [x] Closes: #46019 
<!-- - [ ] 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
2026-03-09 17:32:57 -05:00
Jiří Polášek
a130969d0a CmdPal: Fix selection desync when clearing search query (#45949)
## Summary of the Pull Request

A single search-text change produces multiple ItemsUpdated callbacks. A
later soft update (ForceFirstItem=false) could overwrite the prior
force-first intent, restoring a stale sticky selection.

- Adds latched _forceFirstPending flag that survives across successive
ItemsUpdated passes until selection stabilizes or user navigates;
- Fixes scroll position on first-item reselection via UpdateLayout +
ScrollToItem + ResetScrollToTop in the deferred callback.

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

- [x] Closes: #45948 
<!-- - [ ] 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
2026-03-09 15:12:53 -04:00
Jaylyn Barbee
d1605640ca [Keyboard Manager] Adding KBM to shortcut list on Dashboard page (#45938)
Found during manual testing
Open (new) Keyboard Manager shortcut is now shown in the "Shortcuts"
menu when the module is enabled and the new editor is being used.
<img width="1453" height="1367" alt="image"
src="https://github.com/user-attachments/assets/05de4337-9420-460c-b579-8f471a49d4f6"
/>
2026-03-09 19:06:56 +01:00
Noraa Junker
9859fb6196 Enhance bug report template with file upload option (#46015)
<!-- 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

Adds an upload field for the bug report ZIP-file to the bug report issue
template.

<img width="982" height="157" alt="image"
src="https://github.com/user-attachments/assets/5b7175d7-eacf-4748-93e8-304a027de005"
/>

## PR Checklist

<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-03-09 18:18:25 +01:00
Mike Griese
3bd85efc56 CmdPal: update template project to 0.9 (#46010)
Update the template project to the
[0.9.260303001](https://www.nuget.org/packages/Microsoft.CommandPalette.Extensions/0.9.260303001)
SDK
2026-03-09 12:01:26 +00:00
Jiří Polášek
f8453214fb CmdPal: add locking TopLevelCommandManager.DockBands (#45898)
## Summary of the Pull Request

- Put enumeration of DockBands in
TopLevelCommandManager.ExtensionService_OnExtensionRemoved under correct
lock
- Return a snapshot of the dock bands list to prevent reading DockBands
without lock outside of TopLevelCommandManager

## PR Checklist

- [x] Closes: #45893
2026-03-09 06:48:58 -05:00
Kai Tao
0aca7c292c Cursor Wrap: Reverse the wrap mode (hold ctrl or shift) to wrap (#46009)
<!-- 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
Currently, hold ctrl or shift to disable wrap, which is inverse with
existing similar thing in mouse without borders,
reverse the behavior, so we hold ctrl or shift to wrap.

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

- [ ] Closes: #46005
<!-- - [ ] 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
Verified locally, everything works as expected, hold ctrl and shift will
trigger the wrap, other wise not if select the corresponding option
2026-03-09 12:21:24 +01:00
Jiří Polášek
c6f1a09fa2 CmdPal: Handle an empty icon in dock items (#45968)
## Summary of the Pull Request

This PR allows dock to handle items without an icon better:

- Hides the icon element when a dock item is not visible and center text
labels in vertical dock modes.
- Adds an icon to clock dock wrapper, so it appears in settings.
- Stretches buttons with icon and labels in the vertical docks to the
full width.
- Fixes `IconInfoViewModel.IconForTheme` method implementation.

## Pictures? Pictures!

Horizontal Dock:

<img width="393" height="84" alt="image"
src="https://github.com/user-attachments/assets/d12aa406-da9d-4bd2-b464-595deab41d2e"
/>

<img width="390" height="105" alt="image"
src="https://github.com/user-attachments/assets/c28d65c0-1023-47d0-9ff5-85c74a18c342"
/>


Vertical Dock:

<img width="153" height="258" alt="image"
src="https://github.com/user-attachments/assets/e1be59d9-fa1f-4a24-b0c1-e8cff4211906"
/>

Settings:

<img width="1266" height="713" alt="image"
src="https://github.com/user-attachments/assets/722d47da-c668-4df2-9f1d-bf7808333be4"
/>



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

- [x] Closes: #45955
2026-03-09 06:17:56 -05:00
Jiří Polášek
b72224ea0b CmdPal: Ensure IconBox uses an initialized scale (#45980)
## Summary of the Pull Request

This PR makes sure that IconBox is using valid scale to prevent
defaulting to 0 and using poorly resizes full-scale image:

<img width="81" height="79" alt="image"
src="https://github.com/user-attachments/assets/55bbb08f-6f78-4c19-b7a6-748176dee9c8"
/>
vs 
<img width="89" height="88" alt="image"
src="https://github.com/user-attachments/assets/2dc26863-88c0-4c47-8798-023611d571b5"
/>


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

- [x] Closes: #45973
2026-03-09 06:15:23 -05:00
Jiří Polášek
e323da939b CmdPal: Don't be smart and stop passing a string file path with a file drop (#45951)
<!-- 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

This PR removes the text data format from the data package attached to
list items for files, keeping only the file drop-related formats.

MS Teams doesn't handle having both formats simultaneously well. :/

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

- [x] Closes: #45950
<!-- - [ ] 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
2026-03-09 05:48:49 -05:00
Kai Tao
75fb296bb2 Fix: Fix a issue that change always on top settings won't take effect immediately (#45994)
<!-- 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
Fix #45993

Always on top have a window proc thread that will reload the settings
once file watcher trigger a reload of settings.
And always on top has a worker thread to read the settings at the same
time.
So it may happen worker thread will read the stale setting, as a result,
user change settings, and try to invoke always on top, as if nothing has
changed.
As the issue's recording shows.

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

- [X] Closes: #45993
<!-- - [ ] 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

The setting take effect once it's changed:


https://github.com/user-attachments/assets/70d753e9-eca1-4040-9abf-4cfa4e8dacec
2026-03-09 11:25:40 +08:00
Kai Tao
3d69785ca4 Cmdpal Powertoys Extension: Support mouse without borders easy mouse … (#45350)
## Description

You don't have to go to powertoys settings to 
* toggle the mouse move from machine to another
* you can trigger reconnect when connection lost from cmdpal
* You can toggle whether kbm is turning on or not from cmdpal

<!-- 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
Add several missing cmd to powretoys cmdpal extension


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

- [ ] Closes: #xxx
<!-- - [ ] 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


https://github.com/user-attachments/assets/9ea019f7-988b-4542-afc5-a80f0fc99ef8

For the kbm toggle, when it's off, kbm will not map anything, if it's
on, kbm will take effect

For the mouse without borders, add these two functionality as command
<img width="1182" height="158" alt="image"
src="https://github.com/user-attachments/assets/27f526b1-9c91-4923-be6c-e505673f5892"
/>

And verified for the two command, works as expected
2026-03-09 10:25:11 +08:00
Kai Tao
f6b0996c9b Chrore: Fix MouseUtils folder structure within slnx view (#45990)
<!-- 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
In current main:
<img width="260" height="216" alt="image"
src="https://github.com/user-attachments/assets/4c710e4d-b6b9-4dc0-8b19-99fc0ca6366f"
/>
The mouse utils projects are put inside the keyboard manager.

So change it back to:
<img width="256" height="367" alt="image"
src="https://github.com/user-attachments/assets/342ea9e9-34ca-462d-a4e6-f4cead1e27b0"
/>

introduced in #45649

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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-08 17:09:53 +01:00
Kai Tao
748d5e485c Chore: Remove new info badge from system (#45992)
<!-- 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
We don't have new module in release 98, so remove the new info badge


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

- [ ] Closes: #xxx
<!-- - [ ] 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
### Current:
<img width="300" height="82" alt="image"
src="https://github.com/user-attachments/assets/31a161e8-b451-4e4b-b111-7a538cefe0f3"
/>

### After fix:
<img width="323" height="167" alt="image"
src="https://github.com/user-attachments/assets/d1ca3c5c-6777-4f69-9760-abc43790bd48"
/>
2026-03-08 17:09:24 +08:00
Kai Tao
1718cecedb Pipeline: Fix the sign fail in release pipeline due to file not found issue (#45971)
<!-- 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
Release pipeline failed at main, here is the issue:
<img width="674" height="52" alt="image"
src="https://github.com/user-attachments/assets/303fa3f2-5207-4509-b27c-9e404986f6f0"
/>

Here is the actual location, within winui3 folder
<img width="508" height="58" alt="image"
src="https://github.com/user-attachments/assets/931e363f-ad7e-4b7a-aca2-7b3e23f5cc72"
/>

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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-08 08:56:23 +08:00
Jiří Polášek
4f0c8f476a CmdPal: Add new colorful icons for Bookmarks and Performance Monitor (#45979)
## Summary of the Pull Request

This PR replaces fluent outline icons used for Bookmarks extension and
Performance Monitor extension to put them in line with other extensions:

## Pictures? Pictures:

| | Old | New |
|-----------------|----------------------|--------------------------|
| Bookmarks | <img width="244" height="84" alt="image"
src="https://github.com/user-attachments/assets/3fb26dd0-1b6b-4b48-b08b-af6ff2bf648d"
/> | <img width="221" height="81" alt="image"
src="https://github.com/user-attachments/assets/4f01eb93-1188-48aa-883f-c02e206bd2d1"
/> |
| Perf Mon | <img width="225" height="68" alt="image"
src="https://github.com/user-attachments/assets/fd917461-0e42-474a-ae67-4d1cf433dfa9"
/> | <img width="218" height="89" alt="image"
src="https://github.com/user-attachments/assets/ba143d4b-f9b3-45aa-9948-d4ebb22abb29"
/> |



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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-07 19:21:39 +00:00
Kai Tao
a953a39aec Chore: PowerToys extension development dev guide, and clean an unused msix declaration (#45967)
<!-- 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
PowerToys extension development dev guide, and clean an unused msix
declaration

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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-07 19:08:53 +08:00
Niels Laute
8c4ff37a50 [CmdPal] Visual dock tweaks (#45954)
## Summary of the Pull Request

- Changes the Dock height to 38px (from 32) to avoid item and app
clipping.
- Localization
- Removing dead code
- If the tooltip string is null or empty, the tooltip will not be shown
- Adding hyperlinks on the General and Dock pages in Settings (to be
updated to the corresponding docs via aka.ms)
- The droptarget for an empty listview is now wider, and has a
highlight-color to communicate an item can be dropped:
<img width="371" height="142" alt="image"
src="https://github.com/user-attachments/assets/6863ca5a-cdd4-450b-ab57-d03d83170cf8"
/>



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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-06 21:27:23 +00:00
Jiří Polášek
02062dd023 CmdPal: Replace MainListPage icon with unplated CmdPal icon to make it bigger (#45958)
## Summary of the Pull Request

This PR updates MainListPage icon of CmdPal bolt with an unplated
version to make it bigger.
Icon in the search bar is unaffected by this change, the top-level icon
is hard coded in the shell page.

<img width="144" height="164" alt="image"
src="https://github.com/user-attachments/assets/840de3c8-675f-4b62-a76b-5fbe0d98575f"
/>


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

- [x] Closes: #45956
<!-- - [ ] 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
2026-03-06 20:54:33 +00:00
Jiří Polášek
bcbca0d5dd CmdPal: Refactor PerformanceMonitor extension GPU stats to use batch counter reads (#45835)
<!-- 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

This PR reduces overall CPU usage caused by GPU statistics in
Performance Monitor extension.

Replaces per-instance PerformanceCounter objects with batch reads via
PerformanceCounterCategory.ReadCategory, reducing kernel transitions and
improving efficiency.


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

- [x] Closes: #45823
<!-- - [ ] 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
2026-03-05 15:34:52 -05:00
Jiří Polášek
f0134e4448 CmdPal: Add adaptive parallel fallback processing and consistent updates (#42273)
## Summary of the Pull Request

This PR replaces sequential fallback processing with an adaptive
parallel dispatch model and isolates fallback work onto a dedicated
thread pool, preventing misbehaving extensions from starving the .NET
ThreadPool on blocked synchronous COM RPC call.

The other major change is allowing MainListPage to allow take control
over the debounce of search updates directly, reducing latency and
improving smoothness of the search.

- Adds `DedicatedThreadPool` — an elastic pool of background threads
(min 2, max 32) that expand on demand when all threads are blocked in
COM calls and shrink after 30s idle.
- Extracts all fallback dispatch machinery (adaptive workers,
per-command inflight tracking, pending-retry slots) from MainListPage
into a standalone FallbackUpdateManager.
- Prevents one fallback from monopolizing all threads by capping
concurrent in-flight calls per fallback to 4.
- Starts with low degree of parallelism (2) and gently scales up to half
of CPU cores per batch. If a fallback takes more than 200ms, another
worker is spawned so remaining commands aren't blocked.
- Adds `ThrottledDebouncedAction` to coalesce rapid `RaiseItemsChanged`
calls from fallback completions and user input (100ms for external
events, 50ms adjusted for keystrokes), replacing unbatched direct calls.
- Bypasses the UI-layer debounce timer for the main list page since it
now handles its own throttling, eliminating double-debounce latency.
- Introduces diagnostics for fallbacks and timing hidden behind feature
flags.


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

- [x] Closes: #42286
- [x] Related to: #44407
- [ ] **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
2026-03-05 15:34:24 -05:00
Zach Teutsch
f651d1a611 [Keyboard Manager] Updated WinUI3 KBM and toggles (#45649)
## Running the Project
**Option 1: Test via runner**
1. Check out branch `niels9001/kbm-ux-consolidation`
2. Build PowerToys project
3. Manually build `Modules/KeyboardManagerEditorUI` project separately
4. Run `runner` project
5. Ensure experimental features are enabled in general settings (should
be on by default)
6. Launch keyboard manager via settings app

**Option 2: Test via installer**
1. Install PowerToys via installer on azure pipeline
1. Launch keyboard manager

## Validation
For each page (Text, Remappings, Programs, URLs):
* Create shortcuts with variable options and ensure they run as expected
* Delete shortcuts and ensure they no longer execute
* Try to create invalid shortcuts to check for proper validation
* Ensure created shortcuts appear in Power Toys Settings Keyboard
manager page
* Try toggling shortcuts
* Try deleting shortcuts while toggled off

### UI
* Any feedback on UI design appreciated as well
<img width="1071" height="671" alt="image"
src="https://github.com/user-attachments/assets/d2e81de0-6d92-4189-9a33-32e94cce74f7"
/>
<img width="2142" height="1341" alt="image"
src="https://github.com/user-attachments/assets/0e4e5685-fdf1-4dfd-ba52-a2e5bc9a66db"
/>



Closes: #15870
Closes: #31902
Closes: #45302
Closes: #36227
Closes: #16093
Closes: #13409
Closes: #9919
Closes:  #9482
Closes: #8798
Closes:  #7054
Closes: #2733
Closes: #2027
Closes: #30167

---------

Co-authored-by: Hao Liu <liuhao3418@gmail.com>
Co-authored-by: chenmy77 <162882040+chenmy77@users.noreply.github.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Jay <65828559+Jay-o-Way@users.noreply.github.com>
Co-authored-by: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com>
Co-authored-by: Dustin L. Howett <duhowett@microsoft.com>
2026-03-04 15:46:42 -05:00
Jiří Polášek
d20ae940d5 CmdPal: Replace FiltersDropDown ComboBox with searchable dropdown (#45747)
## Summary of the Pull Request

Replaces the ComboBox-based filter control with a DropDownButton and
Flyout containing a searchable TextBox and ListView.

- Add type-to-search: typing while button is focused opens the flyout
and filters items by name
- Designed to match appearance of the context menu
- Add keyboard navigation: `Up`/`Down `moves selection from search box,
`Enter` confirms, `Escape` clears search text (or closes if empty), `F4`
opens the dropdown
- Add `Alt+F` shortcut on ShellPage to toggle filter focus
- Style flyout to match ContextMenu (item padding, separators, search
box appearance)
- Show "No results" empty state when search matches nothing
- After confirming selection, return focus to the main search box
- Add accessibility
- Update `FilterTemplateSelector` to support both ComboBoxItem and
ListViewItem containers
- Guard against infinite loop in navigation when only separators exist

## Pictures? Moving!



https://github.com/user-attachments/assets/60e232ae-8cee-4759-a9a7-d7edbf78719e

<img width="315" height="212" alt="image"
src="https://github.com/user-attachments/assets/b6e1a895-064c-47e1-9184-26dbb46fdf05"
/>


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

- [x] Closes: #41648
<!-- - [ ] 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
2026-03-04 13:05:22 -06:00
Niels Laute
86860df314 [Cursor Wrap] Update edge wrap model, update simulator, add cursor logging, add settings support to ModuleLoader (#45915)
This PR adds new options for disabling wrap, updates the wrapping model,
extends the simulator and cursor logging.

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

- [ ] Closes: #45116 
- [ ] Closes: #44955 
- [ ] Closes: #44827 
- [ ] **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
- [x] **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

The PR adds a new option for disabling cursor wrapping, exposing three
options: None - wrapping is not disabled, Ctrl key - if this is pressed
then wrapping is disabled, Shift key - if this is pressed then wrapping
is disabled, this would enable a user to temporarily disable wrapping if
they wanted to get close to a monitor edge without wrapping (auto-hide
status bar for example).

The cursor wrap edge model has been updated to mirror Windows
monitor-to-monitor cursor movement, this should ensure there aren't any
non-wrappable edges.

A new test tool has been added 'CursorLog' this is a monitor aware,
dpi/scaling aware Win32 application that captures mouse movement across
monitors to a log file, the log contains one line per mouse movement
which includes: Monitor, x, y, scale, dpi.

The wrapping simulator has been updated to include the new wrapping
model and support mouse cursor log playback.

## Validation Steps Performed
The updated CursorWrap has been tested on a single monitor (laptop) and
multi-monitor desktop PC with monitors being offset to test
edge/wrapping behavior.

---------

Co-authored-by: Mike Hall <mikehall@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: vanzue <vanzue@outlook.com>
2026-03-04 13:56:32 +00:00
Kai Tao
d28f312b81 Copilot Skills: Release note generation skill should also quote co-authors (#45819)
<!-- 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

Release note generation skill add support for quoting co-authors

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

- [ ] Closes: #xxx
<!-- - [ ] 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

Result may vary because of uncertainty of LLM, while I can get this
result during my tests

<img width="696" height="147" alt="image"
src="https://github.com/user-attachments/assets/9d20670f-b9fb-4630-b6b4-f94c2a5d2284"
/>
2026-03-04 16:40:08 +08:00
Mike Griese
f6309ac549 cmdpal: add IDs to all PT commands (#45840)
Adds IDs to all the PT extension commands. This will let all the PT
commands be pinned, ala #45191
2026-03-04 02:25:56 -05:00
Jiří Polášek
c23ba227b4 CmdPal: Debounce SelectedItem updates in CommandBarViewModel (#45782)
<!-- 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

This PR adds DispatcherQueueTimer-based debounce to SelectedItem updates
when receiving UpdateCommandBarMessage, preventing rapid consecutive
changes and prevents blinking when items change to fast (e.g. during
search). That's right - Command Palette is too fast!

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

- [x] Closes: #45776
<!-- - [ ] 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
2026-03-03 12:22:41 -06:00
Jiří Polášek
ce2e72832c CmdPal: Resilient loading of extensions (#45720)
## Summary of the Pull Request

This PR improves the loading of extensions in the Command Palette and
allows extensions that missed the initial timeout to finish loading.

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

- [x] Closes: #45711
<!-- - [ ] 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
2026-03-03 11:56:44 -06:00
Jiří Polášek
c066cc3deb CmdPal: Fix window restore when the window is not WS_EX_TOOLWINDOW (#45877)
## Summary of the Pull Request

This PR checks the window’s actual extended style before saving the
current window size so the offset is calculated correctly. The API used
can return coordinates in different coordinate spaces depending on
whether the window has the `WS_EX_TOOLWINDOW` extended style.

This makes sense in case that settings `WS_EX_TOOLWINDOW` fails, or is
not applied (when debugger is attached).

>
https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-windowplacement
> If the window is a top-level window that does not have the
WS_EX_TOOLWINDOW window style, then the coordinates represented by the
following members are in workspace coordinates: ptMinPos
2026-03-03 10:28:17 -06:00
Jiří Polášek
9089ca2ede CmdPal: Expand Binding markup extensions to nested elements to avoid WMC1510 (#45830)
## Summary of the Pull Request

This PR expands all Binding XAML markup expressions to nested elements,
which in turn prevents compiler from generating `WMC1510 Ensure the
property path is trimming and AOT compatible` warnings.

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

- [x] Related to: #42574
2026-03-03 04:46:23 -06:00
Niels Laute
798564eea4 CmdPal Dock visual bugfixes (#45871)
This PR fixes the following bugs and iprovements:

- The scroll forward/backward glyphs were clipped
- The `Show labels` settings is redundant as those can be set
individually, and actually messes with that setting. Therefore, removed
from the Settings UI.
- Minor string changes
- The 'Filter commands' box is hidden when right clicking the Dock to
customize it.

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2026-03-03 10:54:28 +01:00
Zach Teutsch
738b78c406 [CI] Update Assets conflict script to improve error message clarity (#45551)
<!-- 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
Very minor but updated Assets validation script on the CI to make error
messages clearer after discussion with @yeelam-gordon
2026-03-03 06:27:52 +00:00
Jiří Polášek
1cb99e32ef CmdPal: Add ability to refresh the list page while preserving selection (#45882)
## Summary of the Pull Request

This PR adds ability to invoke soft/incremental refresh that updates
items but keep selection in place (instead of resetting it to the first
item). For now, this is implemented as a hack using an unused parameter
of `ListPage.RaiseItemsChanged`: passing the constant `-2` tells the
view model that this is an incremental change and that it should keep
the current selection.


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

- [x] Closes: #45694
- [x] Related to: #44407
<!-- - [ ] 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
2026-03-03 00:39:49 -05:00
Kai Tao
95835a4cfa Always On Top: Dedup the alwaysontop command id in window system menu (#45845)
<!-- 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
We picked a command id for always on top, although it has little
possibility, but may collide with other window system menu item, so
before inject, try to see if it persists, if yes, then we don't inject.


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

- [ ] Closes: #xxx
<!-- - [ ] 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
A window with the same id
<img width="1049" height="301" alt="image"
src="https://github.com/user-attachments/assets/ae0ba9b7-c46c-4cbf-8994-e0dc4e5c3527"
/>

You can see there is no always ontop overriding the test item

Normal window:
<img width="468" height="597" alt="image"
src="https://github.com/user-attachments/assets/2ba034a8-c41e-4233-9435-4323c8f1c7a4"
/>
2026-03-03 00:34:00 -05:00
Niels Laute
4146876d88 Adding Open Settings button in SCOOBE (#45775)
See title
2026-03-03 00:07:34 -05:00
Jiří Polášek
a6e49c941d CmdPal: Rename "Results" section on Home page to "Commands" (#45870)
## Summary of the Pull Request

This PR renames "Results" section to "Commands" on Home page in default
view, while keeping "Results" for the search results.

Default view (section title changed to "Commands"):

<img width="907" height="591" alt="image"
src="https://github.com/user-attachments/assets/2aa3810f-b1bc-45a1-97f7-e08e6e8b171a"
/>

Search ("Results" are still present):

<img width="906" height="581" alt="image"
src="https://github.com/user-attachments/assets/99620971-55b5-4f49-832e-3dc4eaadba57"
/>


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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-02 18:29:16 -05:00
Jiří Polášek
734c738751 CmdPal: Move ListViewModel.TextToSuggest property update back to UI thread (#45878)
## Summary of the Pull Request

This PR marshals the setting of the property ListViewModel.TextToSuggest
in SetSelectedItem to the UI thread, so that the change notification is
raised on that thread as well.

Regressed in #45764 

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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-02 17:47:02 -05:00
Jaylyn Barbee
22b4dda3aa [Light Switch] Add 10s timeout and pre-check for location detection (#45887)
<!-- 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)?
-->
- Add 10-second timeout to GetGeopositionAsync to prevent infinite
spinner
- Pre-check location services availability when dialog opens; disable
Detect Location button with message if unavailable
- Show user-friendly error messages for timeout and unavailable
scenarios
- Add LocationErrorText UI element and localized string resources

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

- [x] Closes: #45860
- [x] Closes: #42852

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-02 15:26:31 -05:00
Dustin L. Howett
fd399045f7 loc: move build to windows-latest; 2019 is d e a d (#45889)
When Azure DevOps says "no image found," well... he's dead, Jim.
2026-03-02 11:09:14 -08:00
Christian Gaarden Gaardmark
7e3f9f0c3f New+: Fixed issue with files and folders containing only numbers (#45439)
## Summary of the Pull Request
Supersedes https://github.com/microsoft/PowerToys/pull/41465

1) Fix for where template file or folder only contained numbers
2) Fix for where hidden files are shown in the list of templates

## PR Checklist
- [x] Closes: #36216
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [n/a] **Tests:** Added/updated and all pass
- [n/a] **Localization:** All end-user-facing strings can be localized
- [n/a] **Dev docs:** Added/updated
- [n/a] **New binaries:** Added on the required places
- [n/a] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [n/a] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [n/a] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [n/a] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [n/a] **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

## Detailed Description of the Pull Request / Additional comments
1) Fix for where template file or folder only contained numbers
    // Filename cases to support
    // type      | filename                             | result
// [file] | 01. First entry.txt | First entry.txt
    // [folder]  | 02. Second entry                     | Second entry
    // [folder]  | 03 Third entry                       | Third entry
// [file] | 04 Fourth entry.txt | Fourth entry.txt
// [file] | 05.Fifth entry.txt | Fifth entry.txt
    // [folder]  | 001231                               | 001231
    // [file]    | 001231.txt                           | 001231.txt
// [file] | 13. 0123456789012345.txt | 0123456789012345.txt

2) Fix for where hidden files are shown in the list of templates
Instead of excluding based on filename (desktop.ini) exclude based on
hidden and system attribute being set

## Validation Steps Performed
### Before fix
Notice
	1) Folders with numbers only aren't displayed on the context menu
2) Files with extension with numbers only show extension on the context
menu
	3) Some hidden files are shown
<img width="1893" height="786" alt="image"
src="https://github.com/user-attachments/assets/3845a541-499f-47a7-ae99-a92886f74214"
/>



### After fixes
#### Scenario 1
New+ Setting: Hide leading digits…: Yes
New+ Setting: Hide file extension: Yes
New+ Setting: Replace variables: No
<img width="1816" height="1185" alt="image"
src="https://github.com/user-attachments/assets/5ed2c205-d5ce-4366-90d9-c08ef4d2881f"
/>


#### Scenario 2
New+ Setting: Hide leading digits…: No
New+ Setting: Hide file extension: No
New+ Setting: Replace variables: No
<img width="1819" height="1197" alt="image"
src="https://github.com/user-attachments/assets/710265d5-94e9-4fee-9a47-a7bbb78b45bd"
/>


#### Scenario 3
New+ Setting: Hide leading digits…: Yes
New+ Setting: Hide file extension: Yes
New+ Setting: Replace variables: Yes


<img width="1816" height="1197" alt="image"
src="https://github.com/user-attachments/assets/45a90cdd-ec21-4425-9de0-c323ec90f149"
/>
2026-03-02 22:16:01 +08:00
Dave Rayment
9e4bf1e3e0 [Run] Replace WindowWalker's brute-force fuzzy matching algorithm with optimal DP solution (#44551)
## Summary of the Pull Request
Window Walker's fuzzy string matching algorithm exhibits exponential
memory usage and execution time when given inputs containing repeated
characters or phrases. When a user has several windows open with long
titles (such as browser windows), it is straightforward to trigger a
pathological case which uses up gigabytes of memory and freezes the UI.
This is exacerbated by Run's lack of thread pruning, meaning work
triggered by older keystrokes consumes CPU and memory until completion.

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

- [x] Closes: #44546
- [x] Closes: #44184
- [ ] **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

## Detailed Description of the Pull Request / Additional comments
The existing algorithm in `FuzzyMatching.cs` is greedy, creating all
possible matching combinations of the search string within the candidate
via its `GetAllMatchIndexes()` method. After this, it selects the best
match and discards the others. This may be considered reasonable for
small search strings, but it causes a combinatorial explosion when there
are multiple possible matches where characters or substrings repeat,
even when the search string is small.

The current brute-force algorithm has time complexity of **O(n * m *
C(n,m))** where **C(n,m)** = **n!/(m!(n-m)!)** and space complexity of
**O(C(n,m) * m)** because it stores all possible match combinations
before choosing the best.

For example, matching `"eeee"` in `"eeeeeeee"` creates **C(8,4)** =
**70** match combinations, which stores 70 lists with 4 integers each,
plus overhead from the LINQ-based list copying and appending:

```csharp
var tempList = results
    .Where(x => x.Count == secondIndex && x[x.Count - 1] < firstIndex)
    .Select(x => x.ToList())   // Creates a full copy of each matching path
    .ToList();                 // Materializes all copies

results.AddRange(tempList);    // Adds lists to results
```

Each potential sub-match may be recalculated many times.

Window Walker queries across all window titles, so this problem will be
magnified if the search text happens to match multiple titles and/or if
a search string containing a single repeated character is used. For
browser windows, where titles may be long, this is especially
problematic, and similarly for Explorer windows with longer paths.

## Proposed solution
The solution presented here is to use a dynamic programming algorithm
which finds the optimal match directly without generating all
possibilities.

In terms of complexity, the new algorithm benefits from a single pass
through its DP table and only has to store two integer arrays which are
sized proportionally to the search and candidate text string lengths; so
**O(n * m)** for both time and space, i.e. polynomial instead of
exponential.

Scoring is equivalent between the old and new algorithms, based strictly
on the minimum match span within the candidate string.

## Implementation notes
The new algorithm tracks the best start index for matches ending at each
position, eliminating the need to store all possible paths. By storing
the "latest best match so far" as you scan through the search text, you
are guaranteed to minimise the span length. To recreate the best match,
a separate table of parent indexes is kept and iterated backwards once
the DP step is complete. Reversing this provides you with the same
result (or equivalent if there are multiple best matches) as the
original algorithm.

For this "minimum-span" fuzzy matching method, this should be optimal as
it only scans once and storage is proportional to the search and
candidate strings only.

## Benchmarks
A verification and benchmarking suite is here:
https://github.com/daverayment/WindowWalkerBench

Results from comparing the old and new algorithms are here:
https://docs.google.com/spreadsheets/d/1eXmmnN2eI3774QxXXyx1Dv4SKu78U96q28GYnpHT0_8/edit?usp=sharing

| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2
| Allocated | Alloc Ratio |
|----------------
|-----------------:|-----------------:|-----------------:|-----------:|----------:|-----------:|-----------:|----------:|-------------:|------------:|
| Old_Normal | 4,034.4 ns | 220.94 ns | 647.98 ns | 1.02 | 0.23 | 1.9760
| - | - | 8.09 KB | 1.00 |
| New_Normal | 804.5 ns | 24.29 ns | 70.47 ns | 0.20 | 0.04 | 0.4339 | -
| - | 1.77 KB | 0.22 |
| Old_Repetitive | 7,624.7 ns | 318.06 ns | 912.57 ns | 1.94 | 0.38 |
3.7079 | - | - | 15.16 KB | 1.87 |
| New_Repetitive | 2,714.6 ns | 109.03 ns | 318.03 ns | 0.69 | 0.13 |
1.6403 | - | - | 6.72 KB | 0.83 |
| Old_Explosion | 881,443,209.3 ns | 26,273,980.96 ns | 76,225,588.43 ns
| 223,872.87 | 39,357.31 | 50000.0000 | 27000.0000 | 5000.0000 |
351885.11 KB | 43,518.16 |
| New_Explosion | 3,225.4 ns | 111.98 ns | 315.84 ns | 0.82 | 0.15 |
1.7738 | - | - | 7.26 KB | 0.90 |
| Old_Explosion_8 | 460,153,862.6 ns | 18,744,417.95 ns | 54,974,137.06
ns | 116,871.93 | 22,719.87 | 25000.0000 | 14000.0000 | 3000.0000 |
173117.13 KB | 21,409.65 |
| New_Explosion_8 | 2,958.3 ns | 78.16 ns | 230.45 ns | 0.75 | 0.13 |
1.5793 | - | - | 6.46 KB | 0.80 |
| Old_Explosion_7 | 189,069,384.8 ns | 3,774,916.46 ns | 6,202,296.49 ns
| 48,020.68 | 7,501.98 | 11000.0000 | 6333.3333 | 2000.0000 | 71603.96
KB | 8,855.37 |
| New_Explosion_7 | 2,667.5 ns | 117.69 ns | 337.68 ns | 0.68 | 0.13 |
1.3924 | - | - | 5.7 KB | 0.70 |
| Old_Explosion_6 | 71,960,114.8 ns | 1,757,017.15 ns | 5,125,301.87 ns
| 18,276.75 | 3,083.86 | 4500.0000 | 2666.6667 | 1333.3333 | 25515.96 KB
| 3,155.60 |
| New_Explosion_6 | 2,232.5 ns | 72.65 ns | 202.52 ns | 0.57 | 0.10 |
1.1978 | - | - | 4.91 KB | 0.61 |
| Old_Explosion_5 | 9,121,126.4 ns | 180,744.42 ns | 228,583.84 ns |
2,316.62 | 358.55 | 1000.0000 | 968.7500 | 484.3750 | 7630.49 KB |
943.67 |
| New_Explosion_5 | 1,917.3 ns | 48.63 ns | 133.95 ns | 0.49 | 0.08 |
1.0109 | - | - | 4.13 KB | 0.51 |
| Old_Explosion_4 | 2,489,593.2 ns | 82,937.33 ns | 236,624.90 ns |
632.32 | 113.96 | 281.2500 | 148.4375 | 74.2188 | 1729.71 KB | 213.92 |
| New_Explosion_4 | 1,598.3 ns | 51.92 ns | 152.28 ns | 0.41 | 0.07 |
0.8163 | - | - | 3.34 KB | 0.41 |
| Old_Explosion_3 | 202,814.0 ns | 7,684.44 ns | 22,293.96 ns | 51.51 |
9.72 | 72.7539 | 0.2441 | - | 298.13 KB | 36.87 |
| New_Explosion_3 | 1,222.5 ns | 26.07 ns | 76.45 ns | 0.31 | 0.05 |
0.6275 | - | - | 2.57 KB | 0.32 |
| Old_Subsequence | 419,417.7 ns | 8,308.97 ns | 22,178.33 ns | 106.53 |
17.23 | 266.6016 | 0.9766 | - | 1090.05 KB | 134.81 |
| New_Subsequence | 2,501.9 ns | 80.91 ns | 233.43 ns | 0.64 | 0.11 |
1.3542 | - | - | 5.55 KB | 0.69 |

(Where "Old_Explosion" is "e" repeated 9 times. Times in nanoseconds or
one millionth of a millisecond.)

It is worth noting that the results show a **single string match**. So
matching "eeeeee" against a 99-character string took 25 MB of memory and
71 milliseconds to compute. For the new algorithm, this is reduced down
to <5KB and 0.002 milliseconds. Even for a three-character repetition,
the new algorithm is >150x faster with <1% of the allocations.

## Real world example
**Before (results still pending after more than a minute):**
<img width="837" height="336" alt="Image"
src="https://github.com/user-attachments/assets/c4c3ae04-6a47-40b9-a2a4-7a4da169f7d5"
/>

**After (instantaneous results):**
<img width="829" height="444" alt="image"
src="https://github.com/user-attachments/assets/055fc4a6-f34f-4bed-a12c-408b52274de2"
/>

## Validation Steps Performed
The verification tests in the benchmark project pass, with results
identical to the original across a number of test cases, including the
pathological cases identified earlier and edge cases such as
single-character searches.

All unit tests under `Wox.Test`, including all 38 `FuzzyMatcherTest`
entries still pass.
2026-03-02 13:45:14 +01:00
Jiří Polášek
cc3c3c0367 Dock: dock visual nits and bits (#45873)
## Summary of the Pull Request

- Improves vertical (left/right) dock layout:
  - Dock items are not clipped;
  - Items with label(s) are stretched to full width;
  - Items without label(s) are centered.
- Top button is in the absolute corner to make clicking to it easier.
- Dock items now have a min width and height 32px.
- Removes a duplicate "No background" label in the expander body on the
Dock settings page.

## Pictures? Pictures!
Vertical layout:
<img width="175" height="1439" alt="image"
src="https://github.com/user-attachments/assets/4f11d9e9-405f-4d9a-8dee-4c8912b88c7c"
/>

Horizontal layout:
<img width="5119" height="49" alt="image"
src="https://github.com/user-attachments/assets/8420ef26-575e-4427-8db8-f9793e3b3572"
/>



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

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-03-01 20:06:42 +01:00
Sean Killeen
637b58b136 Setup-dev-envronment.ps1: Capture prereleases from vswhere (#45813)
<!-- 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

Takes one possible approach to allowing for preview installs to be
detected.

⚠️ This has a possible side effect, in that if folks have installations
side-by-side, this would seem to update the Preview version over the
non-Preview version. That may not be preferable behavior, in which case
we could instead update `$commonPaths` to include it, which would allow
it to be found in the absence of a non-preview release.

If others have a preference I'm happy to adapt, as long as it doesn't
leave Preview users with an error.

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

- [x] Closes: #45811
<!-- - [ ] 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
- [x] ~~**Tests:** Added/updated and all pass~~ N/A in this case
- [x] ~~**Localization:** All end-user-facing strings can be localized~~
N/A
- [x] ~~**Dev docs:** Added/updated~~ N/A in this case I believe
- [x] ~~**New binaries:** Added on the required places~~ N/A
- [ ] **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

The execution of vsWhere was not returning any items in cases where
prereleases were installed.

This change includes prereleases in the consideration of `-latest`

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

 Tested vswhere output directly. Before: no installations. After: 2026
Preview was found

 Tested setup-dev-environment.ps1. Before: warning about missing VS
install. After: operates as expected.
2026-03-01 17:44:16 +00:00
PesBandi
6c691f59e8 [OOBE] Properly localize View on GitHub and release date in What's new (#45847)
## Summary of the Pull Request
* Move hardcoded string *View on GitHub* to `Resources.resw`
* Respect `DateTimeFormat.MonthDayPattern` in release date
* Dots in the date are escaped so that they don't accidentally trigger a
markdown list
## PR Checklist

- [ ] Closes: #xxx
- [ ] **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
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** No need
- [x] **New binaries:** None
- [x] **Documentation updated:** No need

## Detailed Description of the Pull Request / Additional comments
Nothing changes in the English version, only difference is in languages
that don't use `MMMM d`.
## Validation Steps Performed
Manually tested with Slovak, Czech, German, English.
2026-03-01 12:45:51 +00:00
Mike Griese
7dfe6c0159 Dock: Fix tab focus order for items (#45837)
Now, once the dock has focus, tab will work the way you'd expect.

* re #45584
* re #45595 - if we do add that KB shortcut, well, reasonable tab focus
would be expected.
2026-03-01 13:15:41 +01:00
Mike Griese
543399b62b dock: shift around the padding for fitts law (#45834)
This makes the buttons hitbox extend all the way to the edges of the
dock, but the visual presentation of these buttons is unchanged.

This lets us adhere to fitts law appropriately.

Closes #45596
Closes #45590
2026-03-01 13:03:40 +01:00
Christian Gaarden Gaardmark
90e81cbfd5 [New+] Hide existing new - remake (#44979)
## Summary of the Pull Request
- Add the ability for users and admins (GPO) to control whether to
display built in New on the context menu.
 - Changes to the setting are immediately reflected in the experience.
 - Built-in New is restored on uninstall.

## PR Checklist
Note: Supersedes https://github.com/microsoft/PowerToys/pull/39843 

- [x] **Closes**: [New+] Replace default New entry #37545 and Replace
"New" with New+ option #37946
- [x] **Communication:** Discussed with @niels9001 - 1/22/2025
- [x] **Tests:** Completed manual test pass see highlight below
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** Updated "doc\devdocs\modules\newplus.md"
- [n/a] **New binaries:** Added on the required places
- [n/a] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
   - [x] [WXS for installer] Updated installer (uninstall custom action)
- [n/a] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [n/a] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [No] **Documentation updated:** Pending, coming soon. (original PR 
https://github.com/MicrosoftDocs/windows-dev-docs/pull/5473)

## Detailed Description of the Pull Request / Additional comments
Added the ability for users' admins' to display Windows built-in New or
not
	
I'm NOT aware of an official supported way to do this, so I'm achieving
this by adding an invalid context menu handler in place of New in the
Computer\HKEY_CURRENT_USER\Software\Classes\Directory\background\ShellEx\ContextMenuHandlers\New
	
Changes are immediate, after applying the change, built-in New is
shown/hidden accordingly
	
	Updates to New+ Settings UI
New setting introduced to track user' preference (saved to
newplus/settings.json)
GPO setting introduced for control New visibility via GPO (GPO wins over
user preference)
	
	Updates to New+ power_module.cpp
When runner is running new plus will also apply built-in New admin GPO
and user preference (GPO wins over user preference) to ensure correct
behavior on setting restore and GPO application.
		
	Updates to installer 
		Uninstall always reenable built-in "New" context menu 
	
	Updated DevDoc
		Added a note on how to manually restore built-in New

## Validation Steps Performed
Windows 11 x64
	Settings UI
	New+ enabled
	New+ disabled
	GPO setting enabled
	GPO settings disabled
	Manually updating newplus/settings.json

Windows 11 ARM64
	I tested the reg hack manually, but didn't go through a full pass. 

Windows 10 x64
	NOT tested. 

Windows 11, Settings, New+ Disabled and no GPO
<img width="1040" height="1002" alt="image"
src="https://github.com/user-attachments/assets/1b827b10-f009-4b0b-954f-d9311d40d201"
/>

Windows 11, Settings, New+ Enabled and no GPO
<img width="1015" height="781" alt="image"
src="https://github.com/user-attachments/assets/a5fa09d3-7fd3-4830-99a4-5f2ac9ce1a38"
/>

Hide built-in New: Off (the default)
<img width="321" height="417" alt="image"
src="https://github.com/user-attachments/assets/355fea60-bbb8-4f11-b648-291aaf0c4a6d"
/>

Hide built-in New: On
<img width="1015" height="87" alt="image"
src="https://github.com/user-attachments/assets/e83e45c4-6b67-443b-b045-26e7dda2cf46"
/>

Modern
<img width="308" height="360" alt="image"
src="https://github.com/user-attachments/assets/b164b240-6e67-410c-8481-7db3ee3225b7"
/>

Classic
<img width="308" height="289" alt="image"
src="https://github.com/user-attachments/assets/e2b6c262-a311-454c-9c76-40cb11ff2970"
/>

Disabling New+ also unhide New
<img width="1031" height="569" alt="image"
src="https://github.com/user-attachments/assets/29b8dae7-8190-4e64-b106-c6861e472a3d"
/>

<img width="308" height="353" alt="image"
src="https://github.com/user-attachments/assets/e1977d6b-dc85-4db4-b9ab-c7bb2b27dde2"
/>



Windows 11, Settings, New+ Enabled and with GPO

Hide built-in New: GPO enabled
<img width="1020" height="691" alt="image"
src="https://github.com/user-attachments/assets/75053ab8-92c6-4d38-b1b8-9b0d8293c207"
/>

Hide built-in New: GPO disabled
<img width="1050" height="161" alt="image"
src="https://github.com/user-attachments/assets/1a50b841-ff01-4662-a923-aee63717c834"
/>
2026-03-01 12:32:38 +01:00
368 changed files with 24168 additions and 2995 deletions

View File

@@ -40,7 +40,6 @@ body:
- Other (please specify in "Steps to Reproduce")
validations:
required: true
- type: dropdown
attributes:
label: Area(s) with issue?
@@ -106,7 +105,13 @@ body:
placeholder: What happened instead?
validations:
required: false
- type: upload
id: bugreportfile
attributes:
label: Upload Bug Report ZIP-file
description: Right-clicking the PowerToys tray icon in the taskbar and selecting “Report bug” generates a ZIP file containing diagnostic information about your setup and PowerToys logs, helping us better understand and troubleshoot the issue.
validations:
required: false
- id: additionalInfo
type: textarea
attributes:

View File

@@ -1,6 +1,7 @@
# COLORS
argb
Bgr
bgra
BLACKONWHITE
BLUEGRAY
@@ -28,6 +29,7 @@ RUS
AYUV
bak
HDP
Bcl
bgcode
Deflatealgorithm
@@ -46,6 +48,7 @@ nupkg
petabyte
resw
resx
runtimeconfig
srt
Stereolithography
terabyte
@@ -297,6 +300,8 @@ pwa
AOT
Aot
ify
TFM
# YML
onefuzz

View File

@@ -16,6 +16,7 @@ adaptivecards
ADDSTRING
ADDUNDORECORD
ADifferent
ADMINS
adml
admx
advfirewall
@@ -67,6 +68,7 @@ ARPINSTALLLOCATION
ARPPRODUCTICON
ARRAYSIZE
ARROWKEYS
arrowshape
asf
AShortcut
ASingle
@@ -128,6 +130,7 @@ bthprops
bti
BTNFACE
bugreport
bugreportfile
BUILDARCH
BUILDNUMBER
buildtransitive
@@ -167,7 +170,11 @@ cim
CImage
cla
CLASSDC
classguid
classmethod
CLASSNOTAVAILABLE
claude
CLEARTYPE
clickable
clickonce
clientside
@@ -199,11 +206,13 @@ colorformat
colorhistory
colorhistorylimit
COLORKEY
colorref
comctl
comdlg
comexp
cominterop
commandpalette
commoncontrols
compmgmt
COMPOSITIONFULL
CONFIGW
@@ -215,6 +224,8 @@ CONTEXTHELP
CONTEXTMENUHANDLER
contractversion
CONTROLPARENT
Convs
cooldown
copiedcolorrepresentation
COPYPEN
COREWINDOW
@@ -225,6 +236,8 @@ cpcontrols
cph
cplusplus
CPower
cpptools
cppvsdbg
cppwinrt
createdump
CREATEPROCESS
@@ -247,6 +260,8 @@ CTLCOLORSTATIC
CURRENTDIR
CURSORINFO
cursorpos
CURSORSHOWING
cursorwrap
customaction
CUSTOMACTIONTEST
CVal
@@ -263,12 +278,14 @@ dacl
datareader
datatracker
Dayof
dbcc
DBID
DBLCLKS
DBLEPSILON
DBPROP
DBPROPIDSET
DBPROPSET
DBT
DCBA
DCOM
DComposition
@@ -284,6 +301,8 @@ DEFAULTFLAGS
DEFAULTICON
defaultlib
DEFAULTONLY
DEFAULTSIZE
defaulttonearest
DEFAULTTONULL
DEFAULTTOPRIMARY
DEFERERASE
@@ -303,11 +322,21 @@ DESKTOPABSOLUTEPARSING
desktopshorcutinstalled
devblogs
devdocs
devenv
DEVICEINTERFACE
devicetype
DEVINTERFACE
devmgmt
DEVMODE
DEVMODEW
DEVNODES
devpal
DEVTYP
dfx
DIALOGEX
diffs
digicert
DINORMAL
DISABLEASACTIONKEY
DISABLENOSCROLL
diskmgmt
@@ -421,6 +450,12 @@ eyetracker
FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
FARPROC
fdw
fdx
FErase
fesf
FFFF
Figma
FILEEXPLORER
FILEFLAGS
FILEFLAGSMASK
@@ -437,6 +472,7 @@ FILESYSPATH
Filetime
FILEVERSION
FILTERMODE
FInc
findfast
FIXEDFILEINFO
FIXEDSYS
@@ -492,6 +528,7 @@ GPOCA
gpp
gpu
gradians
GRGX
GSM
gtm
guiddata
@@ -522,11 +559,13 @@ HCRYPTPROV
hcursor
hcwhite
hdc
HDEVNOTIFY
hdr
hdrop
hdwwiz
Helpline
helptext
hgdiobj
HGFE
hglobal
hhk
@@ -537,6 +576,8 @@ HIBYTE
hicon
HIDEWINDOW
Hif
highlightbackground
highlightthickness
HIMAGELIST
himl
hinst
@@ -627,6 +668,7 @@ inetcpl
Infobar
INFOEXAMPLE
Infotip
initialfile
INITDIALOG
INITGUID
INITTOLOGFONTSTRUCT
@@ -668,6 +710,7 @@ jfif
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
jjw
jobject
JOBOBJECT
jpe
jpnime
Jsons
@@ -701,6 +744,7 @@ Ldone
Ldr
LEFTSCROLLBAR
LEFTTEXT
leftclick
LError
LEVELID
LExit
@@ -732,6 +776,8 @@ lowlevel
LOWORD
lparam
LPBITMAPINFOHEADER
LPCFHOOKPROC
lpch
LPCITEMIDLIST
LPCLSID
lpcmi
@@ -749,6 +795,7 @@ LPMONITORINFO
LPOSVERSIONINFOEXW
LPQUERY
lprc
LPrivate
LPSAFEARRAY
lpstr
lpsz
@@ -790,10 +837,13 @@ MAPPEDTOSAMEKEY
MAPTOSAMESHORTCUT
MAPVK
MARKDOWNPREVIEWHANDLERCPP
MAXDWORD
MAXSHORTCUTSIZE
maxversiontested
MBM
MBR
Mbuttondown
mcp
MDICHILD
MDL
mdtext
@@ -805,7 +855,10 @@ MENUITEMINFO
MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metacharacter
metadatamatters
Metadatas
Metacharacter
metafile
metapackage
mfc
@@ -831,6 +884,7 @@ mmsys
mobileredirect
mockapi
MODALFRAME
modelcontextprotocol
MODESPRUNED
MONITORENUMPROC
MONITORINFO
@@ -865,7 +919,9 @@ MSLLHOOKSTRUCT
Mso
msrc
msstore
mstsc
msvcp
MT
MTND
MULTIPLEUSE
multizone
@@ -874,6 +930,8 @@ mvvm
MVVMTK
MWBEx
MYICON
myorg
myrepo
NAMECHANGE
namespaceanddescendants
nao
@@ -976,6 +1034,7 @@ NTAPI
ntdll
NTSTATUS
NTSYSAPI
nullability
NULLCURSOR
nullonfailure
numberbox
@@ -987,6 +1046,8 @@ OEMCONVERT
officehubintl
OFN
ofs
OICI
OICIIO
oldcolor
olditem
oldpath
@@ -997,6 +1058,7 @@ openas
opencode
OPENFILENAME
opensource
openurl
openxmlformats
OPTIMIZEFORINVOKE
ORPHANEDDIALOGTITLE
@@ -1017,6 +1079,9 @@ OWNDC
OWNERDRAWFIXED
Packagemanager
PACL
padx
pady
PAI
PAINTSTRUCT
PALETTEWINDOW
PARENTNOTIFY
@@ -1189,6 +1254,7 @@ RAWPATH
rbhid
rclsid
RCZOOMIT
rdp
RDW
READMODE
READOBJECTS
@@ -1216,6 +1282,7 @@ remappings
REMAPSUCCESSFUL
REMAPUNSUCCESSFUL
Remotable
remotedesktop
remoteip
Removelnk
renamable
@@ -1246,6 +1313,7 @@ RIGHTSCROLLBAR
riid
RKey
RNumber
rollups
rop
ROUNDSMALL
rpcrt
@@ -1278,7 +1346,7 @@ SCREENFONTS
screensaver
screenshots
scrollviewer
SDDL
sddl
SDKDDK
sdns
searchterm
@@ -1448,6 +1516,7 @@ STYLECHANGING
subkeys
sublang
SUBMODULEUPDATE
sug
Superbar
sut
svchost
@@ -1456,6 +1525,9 @@ SVGIO
svgz
SVSI
SWFO
swp
SWPNOSIZE
SWPNOZORDER
SWRESTORE
symbolrequestprod
SYMCACHE
@@ -1472,6 +1544,8 @@ SYSKEY
syskeydown
SYSKEYUP
SYSLIB
sysmenu
systemai
SYSTEMAPPS
SYSTEMMODAL
SYSTEMTIME
@@ -1520,6 +1594,7 @@ tlc
TPMLEFTALIGN
TPMRETURNCMD
TNP
Toggleable
Toolhelp
toolwindow
TOPDOWNDIB
@@ -1557,6 +1632,9 @@ UHash
UIA
UIEx
ULONGLONG
Ultrawide
UMax
UMin
ums
uncompilable
UNCPRIORITY
@@ -2032,6 +2110,7 @@ metadatamatters
middleclickaction
MIIM
mikeclayton
mikehall
minimizebox
modelcontextprotocol
mousehighlighter
@@ -2152,6 +2231,7 @@ taskbar
TESTONLY
TEXTBOXNEWLINE
textextractor
textvariable
tgamma
THEMECHANGED
thickframe
@@ -2191,6 +2271,7 @@ wft
wikimedia
wikipedia
windowedge
WINDOWSAPPRUNTIME
windowsml
winexe
winforms

View File

@@ -289,3 +289,6 @@ St&yle
# Microsoft Store URLs and product IDs
ms-windows-store://\S+
# ANSI color codes
(?:\\(?:u00|x)1[Bb]|\\03[1-7]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+)*m

View File

@@ -33,4 +33,4 @@ These are auto-applied based on file location:
## Detailed Documentation
- [Architecture](../doc/devdocs/core/architecture.md)
- [Coding Style](../doc/devdocs/development/style.md)
- [Coding Style](../doc/devdocs/development/style.md)

View File

@@ -33,7 +33,7 @@ Generated Files/ReleaseNotes/
## Prerequisites
- GitHub CLI (`gh`) installed and authenticated
- **GitHub CLI (`gh`) installed and authenticated** — The collection script uses `gh pr view` and `gh api graphql` to fetch PR metadata and co-author information. Run `gh auth status` to verify; if not logged in, run `gh auth login` first. See [Step 1.0.0](./references/step1-collection.md) for details.
- MCP Server: github-mcp-server installed
- GitHub Copilot code review enabled for the org/repo
@@ -49,6 +49,10 @@ Generated Files/ReleaseNotes/
```
┌────────────────────────────────┐
│ 1.0 Verify gh auth + MemberList │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 1.1 Collect PRs (stable range) │
└────────────────────────────────┘
@@ -85,6 +89,7 @@ Generated Files/ReleaseNotes/
| Step | Action | Details |
|------|--------|---------|
| 1.0 | Verify prerequisites | `gh auth status` must pass; generate MemberList.md |
| 1.1 | Collect PRs | From previous release tag on `stable` branch → `sorted_prs.csv` |
| 1.2 | Assign Milestones | Ensure all PRs have correct milestone |
| 2.12.4 | Label PRs | Auto-suggest + human label low-confidence |

View File

@@ -1,9 +1,9 @@
- Added mouse button actions so you can choose what left, right, or middle click does. Thanks [@PesBandi](https://github.com/PesBandi)!
- Added mouse button actions so you can choose what left, right, or middle click does in [#1234](https://github.com/microsoft/PowerToys/pull/1234) by [@PesBandi](https://github.com/PesBandi)
- Aligned window styling with current Windows theme for a cleaner look. Thanks [@sadirano](https://github.com/sadirano)!
- Aligned window styling with current Windows theme for a cleaner look in [#1235](https://github.com/microsoft/PowerToys/pull/1235) by [@sadirano](https://github.com/sadirano)
- Ensured screen readers are notified when the selected item in the list changes for better accessibility.
- Ensured screen readers are notified when the selected item in the list changes for better accessibility in [#1236](https://github.com/microsoft/PowerToys/pull/1236)
- Implemented configurable UI test pipeline that can use pre-built official releases instead of building everything from scratch, reducing test execution time from 2+ hours.
- Implemented configurable UI test pipeline that can use pre-built official releases instead of building everything from scratch, reducing test execution time from 2+ hours in [#1237](https://github.com/microsoft/PowerToys/pull/1237)
- Fixed Alt+Left Arrow navigation not working when search box contains text. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed Alt+Left Arrow navigation not working when search box contains text in [#1238](https://github.com/microsoft/PowerToys/pull/1238) by [@jiripolasek](https://github.com/jiripolasek)

View File

@@ -1,6 +1,7 @@
# Step 1: Collection and Milestones
## 1.0 To-do
- 1.0.0 Verify GitHub CLI authentication (REQUIRED)
- 1.0.1 Generate MemberList.md (REQUIRED)
- 1.1 Collect PRs
- 1.2 Assign Milestones (REQUIRED)
@@ -20,6 +21,34 @@
---
## 1.0.0 Verify GitHub CLI Authentication (REQUIRED)
⚠️ **BLOCKING:** The collection script requires an authenticated `gh` CLI to fetch PR metadata and co-author information via GitHub's GraphQL API. Without authentication, PR data and `NeedThanks` attribution will be incomplete.
### Check authentication status
```powershell
gh auth status
```
**If authenticated:** You'll see `Logged in to github.com account <username>`. Proceed to 1.0.1.
**If NOT authenticated:** Run the login flow before continuing:
```powershell
# Interactive login (opens browser for OAuth)
gh auth login --hostname github.com --web
# Or use a personal access token
gh auth login --with-token <<< "YOUR_GITHUB_TOKEN"
```
**Required scopes:** `repo` (for reading PR data and assigning milestones)
After login, verify again with `gh auth status` and confirm exit code 0.
---
## 1.0.1 Generate MemberList.md (REQUIRED)
Create `Generated Files/ReleaseNotes/MemberList.md` from the **PowerToys core team** section in [COMMUNITY.md](../../../COMMUNITY.md).
@@ -80,6 +109,8 @@ The script detects both merge commits (`Merge pull request #12345`) and squash c
**Output** (in `Generated Files/ReleaseNotes/`):
- `milestone_prs.json` - raw PR data
- `sorted_prs.csv` - sorted PR list with columns: Id, Title, Labels, Author, Url, Body, CopilotSummary, NeedThanks
- `Author`: Comma-separated list of all contributors (PR opener + co-authors from commit trailers)
- `NeedThanks`: Comma-separated list of external contributors to thank (excludes core team members from MemberList.md). Empty string means no thanks needed.
---

View File

@@ -16,7 +16,7 @@ For each CSV in `Generated Files/ReleaseNotes/grouped_csv/`, create a markdown f
- Use the “Verb-ed + Scenario + Impact” sentence structure—make readers think, “Thats exactly what I need” or “Yes, thats an awesome fix.”; The "impact" can be end-user focused (written to convey user excitement) or technical (performance/stability) when user-facing impact is minimal.
- If nothing special on impact or unclear impact, mark as needing human summary
- Source from Title, Body, and CopilotSummary (prefer CopilotSummary when available)
- If the column `NeedThanks` in CSV is `True`, append: `Thanks [@Author](https://github.com/Author)!`
- The `NeedThanks` column contains a comma-separated list of external contributor usernames who should be thanked (empty = no thanks needed, all authors are core team). For each non-empty `NeedThanks` value, append thanks for **every** listed contributor: `Thanks [@user1](https://github.com/user1)!` for a single contributor, or `Thanks [@user1](https://github.com/user1) and [@user2](https://github.com/user2)!` for two, or `Thanks [@user1](https://github.com/user1), [@user2](https://github.com/user2), and [@user3](https://github.com/user3)!` for three or more.
- Do NOT include PR numbers in bullet lines
- Do NOT mention “security” or “privacy” issues, since these are not known and could be leveraged by attackers in earlier versions. Instead, describe the user-facing scenario, usage, or impact.
- If confidence < 70%, write: `Human Summary Needed: <PR full link>`
@@ -72,13 +72,13 @@ Some items in the Development section may overlap and should be moved to the Mod
## Advanced Paste
- Wrapped paste option lists in a single ScrollViewer
- Added image input handling for AI-powered transformations
- Wrapped paste option lists in a single ScrollViewer in [#5678](https://github.com/microsoft/PowerToys/pull/5678)
- Added image input handling for AI-powered transformations in [#5679](https://github.com/microsoft/PowerToys/pull/5679)
...
## Awake
- Fixed timed mode expiration. Thanks [@daverayment](https://github.com/daverayment)!
- Fixed timed mode expiration in [#5680](https://github.com/microsoft/PowerToys/pull/5680) by [@daverayment](https://github.com/daverayment)
...
---

View File

@@ -42,30 +42,7 @@ param(
[string]$OutputJson = "milestone_prs.json"
)
<#
.SYNOPSIS
Dump merged PR information whose merge commits are reachable from EndCommit but not from StartCommit.
.DESCRIPTION
Uses git rev-list to compute commits in the (StartCommit, EndCommit] range, extracts PR numbers from merge commit messages,
queries GitHub (gh CLI) for details, then outputs a CSV.
PR merge commit messages in PowerToys generally contain patterns like:
Merge pull request #12345 from ...
.EXAMPLE
pwsh ./dump-prs-since-commit.ps1 -StartCommit 0123abcd -Branch stable
.EXAMPLE
pwsh ./dump-prs-since-commit.ps1 -StartCommit 0123abcd -EndCommit 89ef7654 -OutputCsv changes.csv
.NOTES
Requires: gh CLI authenticated; git available in working directory (must be inside PowerToys repo clone).
CopilotSummary behavior:
- Attempts to locate the latest GitHub Copilot authored review (preferred).
- If no review is found, lazily fetches PR comments to look for a Copilot-authored comment.
- Normalizes whitespace and strips newlines. Empty when no Copilot activity detected.
- Run with -Verbose to see whether the summary came from a 'review' or 'comment' source.
#>
# (See top-level synopsis above for full documentation)
function Write-Info($msg) { Write-Host $msg -ForegroundColor Cyan }
function Write-Warn($msg) { Write-Host $msg -ForegroundColor Yellow }
@@ -151,11 +128,11 @@ catch {
}
Write-Info "Collecting commits between $startSha..$endSha (excluding start, including end)."
# Get list of commits reachable from end but not from start.
# IMPORTANT: In PowerShell, the .. operator creates a numeric/char range. If $startSha and $endSha look like hex strings,
# `$startSha..$endSha` must be passed as a single string argument.
$rangeArg = "$startSha..$endSha"
$commitList = git rev-list $rangeArg
# Get list of commits reachable from end but not from start.
# IMPORTANT: In PowerShell, the .. operator creates a numeric/char range. If $startSha and $endSha look like hex strings,
# `$startSha..$endSha` must be passed as a single string argument.
$rangeArg = "$startSha..$endSha"
$commitList = git rev-list $rangeArg
# Normalize list (filter out empty strings)
$normalizedCommits = $commitList | Where-Object { $_ -and $_.Trim() -ne '' }
@@ -210,6 +187,63 @@ $prNumbers = $mergeCommits | Select-Object -ExpandProperty Pr -Unique | Sort-Obj
Write-Info ("Found {0} unique PRs: {1}" -f $prNumbers.Count, ($prNumbers -join ', '))
Write-DebugMsg ("Total merge commits examined: {0}" -f $mergeCommits.Count)
# Build a map of PR number → list of commit SHAs (for co-author extraction)
$prToCommits = @{}
foreach ($mc in $mergeCommits) {
if (-not $prToCommits.ContainsKey($mc.Pr)) {
$prToCommits[$mc.Pr] = @()
}
$prToCommits[$mc.Pr] += $mc.Sha
}
<#
.SYNOPSIS
Get all authors (including co-authors) for a set of commits via GitHub GraphQL API.
.DESCRIPTION
Uses the Commit.authors field in GitHub's GraphQL API which natively includes
co-authors (from Co-authored-by trailers). Returns GitHub usernames (login)
without any email parsing — GitHub resolves the association for us.
NOTE: For squash merges this captures all co-authors correctly because GitHub
preserves Co-authored-by trailers in the squash commit. For traditional merge
commits, only the merger's author is returned — co-authors on individual PR
commits are not traversed. This is acceptable because PowerToys primarily uses
squash merging.
#>
function Get-CommitAuthors {
param(
[string[]]$CommitShas,
[string]$RepoFullName = "microsoft/PowerToys"
)
$parts = $RepoFullName -split '/'
$owner = $parts[0]
$repoName = $parts[1]
$allAuthors = @()
foreach ($sha in $CommitShas) {
try {
$query = "{ repository(owner: `"$owner`", name: `"$repoName`") { object(expression: `"$sha`") { ... on Commit { authors(first: 20) { nodes { user { login } name } } } } } }"
$result = gh api graphql -f query="$query" 2>$null | ConvertFrom-Json
$nodes = $result.data.repository.object.authors.nodes
if ($nodes) {
foreach ($node in $nodes) {
if ($node.user -and $node.user.login) {
$allAuthors += $node.user.login
} else {
# User without a GitHub account (rare) — use display name as fallback
Write-DebugMsg "Commit $sha has an author without GitHub account: $($node.name)"
}
}
}
}
catch {
Write-DebugMsg "GraphQL authors query failed for commit ${sha}: $_"
}
}
return $allAuthors | Select-Object -Unique
}
# Query GitHub for each PR
$prDetails = @()
function Get-CopilotSummaryFromPrJson {
@@ -307,22 +341,45 @@ foreach ($pr in $prNumbers) {
$bodyValue = if ($json.body) { ($json.body -replace "`r", '') -replace "`n", ' ' } else { '' }
$bodyValue = $bodyValue -replace '\s+', ' '
# Determine if author needs thanks (not in member list)
# Collect all contributors: PR author + co-authors from commit messages
$authorLogin = $json.author.login
$needThanks = $true
if ($memberList.Count -gt 0 -and $authorLogin) {
$needThanks = -not ($memberList -contains $authorLogin)
$allContributors = @($authorLogin)
# Extract all authors (including co-authors) from associated commits via GitHub GraphQL API
if ($prToCommits.ContainsKey([int]$pr)) {
$commitAuthors = Get-CommitAuthors -CommitShas $prToCommits[[int]$pr] -RepoFullName $Repo
if ($commitAuthors) {
$allContributors += $commitAuthors
}
}
# Deduplicate contributors (case-insensitive)
$allContributors = $allContributors | Where-Object { $_ } | Sort-Object -Unique
# Filter to only external contributors (not in member list) for thanks
$externalContributors = @()
if ($memberList.Count -gt 0) {
$externalContributors = $allContributors | Where-Object { -not ($memberList -contains $_) }
} else {
$externalContributors = $allContributors
}
# Author column: all contributors (comma-separated)
$authorField = ($allContributors -join ', ')
# NeedThanks column: comma-separated list of external contributors who
# deserve thanks attribution. Empty string means no thanks needed.
$needThanksField = ($externalContributors -join ', ')
$prDetails += [PSCustomObject]@{
Id = $json.number
Title = $json.title
Labels = $labelNames
Author = $authorLogin
Author = $authorField
Url = $json.url
Body = $bodyValue
CopilotSummary = $copilot.Summary
NeedThanks = $needThanks
NeedThanks = $needThanksField
}
}
catch {

View File

@@ -0,0 +1,21 @@
The MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

192
.github/skills/winmd-api-search/SKILL.md vendored Normal file
View File

@@ -0,0 +1,192 @@
---
name: winmd-api-search
description: 'Find and explore Windows desktop APIs. Use when building features that need platform capabilities — camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc. Discovers the right API for a task and retrieves full type details (methods, properties, events, enumeration values).'
license: Complete terms in LICENSE.txt
---
# WinMD API Search
This skill helps you find the right Windows API for any capability and get its full details. It searches a local cache of all WinMD metadata from:
- **Windows Platform SDK** — all `Windows.*` WinRT APIs (always available, no restore needed)
- **WinAppSDK / WinUI** — bundled as a baseline in the cache generator (always available, no restore needed)
- **NuGet packages** — any additional packages in restored projects that contain `.winmd` files
- **Project-output WinMD** — class libraries (C++/WinRT, C#) that produce `.winmd` as build output
Even on a fresh clone with no restore or build, you still get full Platform SDK + WinAppSDK coverage.
## When to Use This Skill
- User wants to build a feature and you need to find which API provides that capability
- User asks "how do I do X?" where X involves a platform feature (camera, files, notifications, sensors, AI, etc.)
- You need the exact methods, properties, events, or enumeration values of a type before writing code
- You're unsure which control, class, or interface to use for a UI or system task
## Prerequisites
- **.NET SDK 8.0 or later** — required to build the cache generator. Install from [dotnet.microsoft.com](https://dotnet.microsoft.com/download) if not available.
## Cache Setup (Required Before First Use)
All query and search commands read from a local JSON cache. **You must generate the cache before running any queries.**
```powershell
# All projects in the repo (recommended for first run)
.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1
# Single project
.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1 -ProjectDir <project-folder>
```
No project restore or build is needed for baseline coverage (Platform SDK + WinAppSDK). For additional NuGet packages, the project needs `dotnet restore` (which generates `project.assets.json`) or a `packages.config` file.
Cache is stored at `Generated Files\winmd-cache\`, deduplicated per-package+version.
### What gets indexed
| Source | When available |
|--------|----------------|
| Windows Platform SDK | Always (reads from local SDK install) |
| WinAppSDK (latest) | Always (bundled as baseline in cache generator) |
| WinAppSDK Runtime | When installed on the system (detected via `Get-AppxPackage`) |
| Project NuGet packages | After `dotnet restore` or with `packages.config` |
| Project-output `.winmd` | After project build (class libraries that produce WinMD) |
> **Note:** This cache directory should be in `.gitignore` — it's generated, not source.
## How to Use
Pick the path that matches the situation:
---
### Discover — "I don't know which API to use"
The user describes a capability in their own words. You need to find the right API.
**0. Ensure the cache exists**
If the cache hasn't been generated yet, run `Update-WinMdCache.ps1` first — see [Cache Setup](#cache-setup-required-before-first-use) above.
**1. Translate user language → search keywords**
Map the user's daily language to programming terms. Try multiple variations:
| User says | Search keywords to try (in order) |
|-----------|-----------------------------------|
| "take a picture" | `camera`, `capture`, `photo`, `MediaCapture` |
| "load from disk" | `file open`, `picker`, `FileOpen`, `StorageFile` |
| "describe what's in it" | `image description`, `Vision`, `Recognition` |
| "show a popup" | `dialog`, `flyout`, `popup`, `ContentDialog` |
| "drag and drop" | `drag`, `drop`, `DragDrop` |
| "save settings" | `settings`, `ApplicationData`, `LocalSettings` |
Start with simple everyday words. If results are weak or irrelevant, try the more technical variation.
**2. Run searches**
```powershell
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action search -Query "<keyword>"
```
This returns ranked namespaces with top matching types and the **JSON file path**.
If results have **low scores (below 60) or are irrelevant**, fall back to searching online documentation:
1. Use web search to find the right API on Microsoft Learn, for example:
- `site:learn.microsoft.com/uwp/api <capability keywords>` for `Windows.*` APIs
- `site:learn.microsoft.com/windows/windows-app-sdk/api/winrt <capability keywords>` for `Microsoft.*` WinAppSDK APIs
2. Read the documentation pages to identify which type matches the user's requirement.
3. Once you know the type name, come back and use `-Action members` or `-Action enums` to get the exact local signatures.
**3. Read the JSON to choose the right API**
Read the file at the path(s) from the top results. The JSON has all types in that namespace — full members, signatures, parameters, return types, enumeration values.
Read and decide which types and members fit the user's requirement.
**4. Look up official documentation for context**
The cache contains only signatures — no descriptions or usage guidance. For explanations, examples, and remarks, look up the type on Microsoft Learn:
| Namespace prefix | Documentation base URL |
|-----------------|----------------------|
| `Windows.*` | `https://learn.microsoft.com/uwp/api/{fully.qualified.typename}` |
| `Microsoft.*` (WinAppSDK) | `https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/{fully.qualified.typename}` |
For example, `Microsoft.UI.Xaml.Controls.NavigationView` maps to:
`https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.navigationview`
**5. Use the API knowledge to answer or write code**
---
### Lookup — "I know the API, show me the details"
You already know (or suspect) the type or namespace name. Go direct:
```powershell
# Get all members of a known type
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.NavigationView"
# Get enum values
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility"
# List all types in a namespace
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls"
# Browse namespaces
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI"
```
If you need full detail beyond what `-Action members` shows, use `-Action search` to get the JSON file path, then read the JSON file directly.
---
### Other Commands
```powershell
# List cached projects
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action projects
# List packages for a project
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action packages
# Show stats
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action stats
```
> If only one project is cached, `-Project` is auto-selected.
> If multiple projects exist, add `-Project <name>` (use `-Action projects` to see available names).
> In scan mode, manifest names include a short hash suffix to avoid collisions; you can pass the base project name without the suffix if it's unambiguous.
## Search Scoring
The search ranks type names and member names against your query:
| Score | Match type | Example |
|-------|-----------|---------|
| 100 | Exact name | `Button``Button` |
| 80 | Starts with | `Navigation``NavigationView` |
| 60 | Contains | `Dialog``ContentDialog` |
| 50 | PascalCase initials | `ASB``AutoSuggestBox` |
| 40 | Multi-keyword AND | `navigation item``NavigationViewItem` |
| 20 | Fuzzy character match | `NavVw``NavigationView` |
Results are grouped by namespace. Higher-scored namespaces appear first.
## Troubleshooting
| Issue | Fix |
|-------|-----|
| "Cache not found" | Run `Update-WinMdCache.ps1` |
| "Multiple projects cached" | Add `-Project <name>` |
| "Namespace not found" | Use `-Action namespaces` to list available ones |
| "Type not found" | Use fully qualified name (e.g., `Microsoft.UI.Xaml.Controls.Button`) |
| Stale after NuGet update | Re-run `Update-WinMdCache.ps1` |
| Cache in git history | Add `Generated Files/` to `.gitignore` |
## References
- [Windows Platform SDK API reference](https://learn.microsoft.com/uwp/api/) — documentation for `Windows.*` namespaces
- [Windows App SDK API reference](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/) — documentation for `Microsoft.*` WinAppSDK namespaces

View File

@@ -0,0 +1,505 @@
<#
.SYNOPSIS
Query WinMD API metadata from cached JSON files.
.DESCRIPTION
Reads pre-built JSON cache of WinMD types, members, and namespaces.
The cache is organized per-package (deduplicated) with project manifests
that map each project to its referenced packages.
Supports listing namespaces, types, members, searching, enum value lookup,
and listing cached projects/packages.
.PARAMETER Action
The query action to perform:
- projects : List cached projects
- packages : List packages for a project
- stats : Show aggregate statistics for a project
- namespaces : List all namespaces (optional -Filter prefix)
- types : List types in a namespace (-Namespace required)
- members : List members of a type (-TypeName required)
- search : Search types and members by name (-Query required)
- enums : List enum values (-TypeName required)
.PARAMETER Project
Project name to query. Auto-selected if only one project is cached.
Use -Action projects to list available projects.
.PARAMETER Namespace
Namespace to query types from (used with -Action types).
.PARAMETER TypeName
Full type name e.g. "Microsoft.UI.Xaml.Controls.Button" (used with -Action members, enums).
.PARAMETER Query
Search query string (used with -Action search).
.PARAMETER Filter
Optional prefix filter for namespaces (used with -Action namespaces).
.PARAMETER CacheDir
Path to the winmd-cache directory. Defaults to "Generated Files\winmd-cache"
relative to the workspace root.
.PARAMETER MaxResults
Maximum number of results to return for search. Defaults to 30.
.EXAMPLE
.\Invoke-WinMdQuery.ps1 -Action projects
.\Invoke-WinMdQuery.ps1 -Action packages -Project BlankWinUI
.\Invoke-WinMdQuery.ps1 -Action stats -Project BlankWinUI
.\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI"
.\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls"
.\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.Button"
.\Invoke-WinMdQuery.ps1 -Action search -Query "NavigationView"
.\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateSet('projects', 'packages', 'stats', 'namespaces', 'types', 'members', 'search', 'enums')]
[string]$Action,
[string]$Project,
[string]$Namespace,
[string]$TypeName,
[string]$Query,
[string]$Filter,
[string]$CacheDir,
[int]$MaxResults = 30
)
# ─── Resolve cache directory ─────────────────────────────────────────────────
if (-not $CacheDir) {
# Convention: skill lives at .github/skills/winmd-api-search/scripts/
# so workspace root is 4 levels up from $PSScriptRoot.
$scriptDir = $PSScriptRoot
$root = (Resolve-Path (Join-Path $scriptDir '..\..\..\..')).Path
$CacheDir = Join-Path $root 'Generated Files\winmd-cache'
}
if (-not (Test-Path $CacheDir)) {
Write-Error "Cache not found at: $CacheDir`nRun: .\Update-WinMdCache.ps1 (from .github\skills\winmd-api-search\scripts\)"
exit 1
}
# ─── Project resolution helpers ──────────────────────────────────────────────
function Get-CachedProjects {
$projectsDir = Join-Path $CacheDir 'projects'
if (-not (Test-Path $projectsDir)) { return @() }
Get-ChildItem $projectsDir -Filter '*.json' | ForEach-Object { $_.BaseName }
}
function Resolve-ProjectManifest {
param([string]$Name)
$projectsDir = Join-Path $CacheDir 'projects'
if (-not (Test-Path $projectsDir)) {
Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
exit 1
}
if ($Name) {
$path = Join-Path $projectsDir "$Name.json"
if (-not (Test-Path $path)) {
# Scan mode appends a hash suffix -- try prefix match
$matching = @(Get-ChildItem $projectsDir -Filter "${Name}_*.json" -ErrorAction SilentlyContinue)
if ($matching.Count -eq 1) {
return Get-Content $matching[0].FullName -Raw | ConvertFrom-Json
}
if ($matching.Count -gt 1) {
$names = ($matching | ForEach-Object { $_.BaseName }) -join ', '
Write-Error "Multiple projects match '$Name'. Specify the full name: $names"
exit 1
}
$available = (Get-CachedProjects) -join ', '
Write-Error "Project '$Name' not found. Available: $available"
exit 1
}
return Get-Content $path -Raw | ConvertFrom-Json
}
# Auto-select if only one project
$manifests = Get-ChildItem $projectsDir -Filter '*.json' -ErrorAction SilentlyContinue
if ($manifests.Count -eq 0) {
Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
exit 1
}
if ($manifests.Count -eq 1) {
return Get-Content $manifests[0].FullName -Raw | ConvertFrom-Json
}
$available = ($manifests | ForEach-Object { $_.BaseName }) -join ', '
Write-Error "Multiple projects cached -- use -Project to specify. Available: $available"
exit 1
}
function Get-PackageCacheDirs {
param($Manifest)
$dirs = @()
foreach ($pkg in $Manifest.packages) {
$dir = Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version
if (Test-Path $dir) {
$dirs += $dir
}
}
return $dirs
}
# ─── Action: projects ────────────────────────────────────────────────────────
function Show-Projects {
$projects = Get-CachedProjects
if ($projects.Count -eq 0) {
Write-Output "No projects cached."
return
}
Write-Output "Cached projects ($($projects.Count)):"
foreach ($p in $projects) {
$manifest = Get-Content (Join-Path (Join-Path $CacheDir 'projects') "$p.json") -Raw | ConvertFrom-Json
$pkgCount = $manifest.packages.Count
Write-Output " $p ($pkgCount package(s))"
}
}
# ─── Action: packages ────────────────────────────────────────────────────────
function Show-Packages {
$manifest = Resolve-ProjectManifest -Name $Project
Write-Output "Packages for project '$($manifest.projectName)' ($($manifest.packages.Count)):"
foreach ($pkg in $manifest.packages) {
$metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json'
if (Test-Path $metaPath) {
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
Write-Output " $($pkg.id)@$($pkg.version) -- $($meta.totalTypes) types, $($meta.totalMembers) members"
} else {
Write-Output " $($pkg.id)@$($pkg.version) -- (cache missing)"
}
}
}
# ─── Action: stats ───────────────────────────────────────────────────────────
function Show-Stats {
$manifest = Resolve-ProjectManifest -Name $Project
$totalTypes = 0
$totalMembers = 0
$totalNamespaces = 0
$totalWinMd = 0
foreach ($pkg in $manifest.packages) {
$metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json'
if (Test-Path $metaPath) {
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
$totalTypes += $meta.totalTypes
$totalMembers += $meta.totalMembers
$totalNamespaces += $meta.totalNamespaces
$totalWinMd += $meta.winMdFiles.Count
}
}
Write-Output "WinMD Index Statistics -- $($manifest.projectName)"
Write-Output "======================================"
Write-Output " Packages: $($manifest.packages.Count)"
Write-Output " Namespaces: $totalNamespaces (may overlap across packages)"
Write-Output " Types: $totalTypes"
Write-Output " Members: $totalMembers"
Write-Output " WinMD files: $totalWinMd"
}
# ─── Action: namespaces ──────────────────────────────────────────────────────
function Get-Namespaces {
param([string]$Prefix)
$manifest = Resolve-ProjectManifest -Name $Project
$dirs = Get-PackageCacheDirs -Manifest $manifest
$allNs = @()
foreach ($dir in $dirs) {
$nsFile = Join-Path $dir 'namespaces.json'
if (Test-Path $nsFile) {
$allNs += (Get-Content $nsFile -Raw | ConvertFrom-Json)
}
}
$allNs = $allNs | Sort-Object -Unique
if ($Prefix) {
$allNs = $allNs | Where-Object { $_ -like "$Prefix*" }
}
$allNs | ForEach-Object { Write-Output $_ }
}
# ─── Action: types ───────────────────────────────────────────────────────────
function Get-TypesInNamespace {
param([string]$Ns)
if (-not $Ns) {
Write-Error "-Namespace is required for 'types' action."
exit 1
}
$manifest = Resolve-ProjectManifest -Name $Project
$dirs = Get-PackageCacheDirs -Manifest $manifest
$safeFile = $Ns.Replace('.', '_') + '.json'
$found = $false
$seen = @{}
foreach ($dir in $dirs) {
$filePath = Join-Path $dir "types\$safeFile"
if (-not (Test-Path $filePath)) { continue }
$found = $true
$types = Get-Content $filePath -Raw | ConvertFrom-Json
foreach ($t in $types) {
if ($seen.ContainsKey($t.fullName)) { continue }
$seen[$t.fullName] = $true
Write-Output "$($t.kind) $($t.fullName)$(if ($t.baseType) { " : $($t.baseType)" } else { '' })"
}
}
if (-not $found) {
Write-Error "Namespace not found: $Ns"
exit 1
}
}
# ─── Action: members ─────────────────────────────────────────────────────────
function Get-MembersOfType {
param([string]$FullName)
if (-not $FullName) {
Write-Error "-TypeName is required for 'members' action."
exit 1
}
$lastDot = $FullName.LastIndexOf('.')
if ($lastDot -lt 0) {
Write-Error "-TypeName must include a namespace (for example: 'MyNamespace.MyType'). Provided: $FullName"
exit 1
}
$ns = $FullName.Substring(0, $lastDot)
$safeFile = $ns.Replace('.', '_') + '.json'
$manifest = Resolve-ProjectManifest -Name $Project
$dirs = Get-PackageCacheDirs -Manifest $manifest
foreach ($dir in $dirs) {
$filePath = Join-Path $dir "types\$safeFile"
if (-not (Test-Path $filePath)) { continue }
$types = Get-Content $filePath -Raw | ConvertFrom-Json
$type = $types | Where-Object { $_.fullName -eq $FullName }
if (-not $type) { continue }
Write-Output "$($type.kind) $($type.fullName)"
if ($type.baseType) { Write-Output " Extends: $($type.baseType)" }
Write-Output ""
foreach ($m in $type.members) {
Write-Output " [$($m.kind)] $($m.signature)"
}
return
}
Write-Error "Type not found: $FullName"
exit 1
}
# ─── Action: search ──────────────────────────────────────────────────────────
# Ranks namespaces by best match score on type names and member names.
# Outputs: ranked namespaces with top matching types and the JSON file path.
# The agent can then read the JSON file to inspect all members intelligently.
function Search-WinMd {
param([string]$SearchQuery, [int]$Max)
if (-not $SearchQuery) {
Write-Error "-Query is required for 'search' action."
exit 1
}
$manifest = Resolve-ProjectManifest -Name $Project
$dirs = Get-PackageCacheDirs -Manifest $manifest
# Collect: namespace -> { bestScore, matchingTypes[], filePath }
$nsResults = @{}
foreach ($dir in $dirs) {
$nsFile = Join-Path $dir 'namespaces.json'
if (-not (Test-Path $nsFile)) { continue }
$nsList = Get-Content $nsFile -Raw | ConvertFrom-Json
foreach ($n in $nsList) {
$safeFile = $n.Replace('.', '_') + '.json'
$filePath = Join-Path $dir "types\$safeFile"
if (-not (Test-Path $filePath)) { continue }
$types = Get-Content $filePath -Raw | ConvertFrom-Json
foreach ($t in $types) {
$typeScore = Get-MatchScore -Name $t.name -FullName $t.fullName -Query $SearchQuery
# Also search member names for matches
$bestMemberScore = 0
$matchingMember = $null
if ($t.members) {
foreach ($m in $t.members) {
$memberName = $m.name
$mScore = Get-MatchScore -Name $memberName -FullName "$($t.fullName).$memberName" -Query $SearchQuery
if ($mScore -gt $bestMemberScore) {
$bestMemberScore = $mScore
$matchingMember = $m.signature
}
}
}
$score = [Math]::Max($typeScore, $bestMemberScore)
if ($score -le 0) { continue }
if (-not $nsResults.ContainsKey($n)) {
$nsResults[$n] = @{ BestScore = 0; Types = @(); FilePaths = @() }
}
$entry = $nsResults[$n]
if ($score -gt $entry.BestScore) { $entry.BestScore = $score }
if ($entry.FilePaths -notcontains $filePath) {
$entry.FilePaths += $filePath
}
if ($typeScore -ge $bestMemberScore) {
$entry.Types += @{ Text = "$($t.kind) $($t.fullName) [$typeScore]"; Score = $typeScore }
} else {
$entry.Types += @{ Text = "$($t.kind) $($t.fullName) -> $matchingMember [$bestMemberScore]"; Score = $bestMemberScore }
}
}
}
}
if ($nsResults.Count -eq 0) {
Write-Output "No results found for: $SearchQuery"
return
}
$ranked = $nsResults.GetEnumerator() |
Sort-Object { $_.Value.BestScore } -Descending |
Select-Object -First $Max
foreach ($r in $ranked) {
$ns = $r.Key
$info = $r.Value
Write-Output "[$($info.BestScore)] $ns"
foreach ($fp in $info.FilePaths) {
Write-Output " File: $fp"
}
# Show top 5 highest-scoring matching types in this namespace
$info.Types | Sort-Object { $_.Score } -Descending |
Select-Object -First 5 |
ForEach-Object { Write-Output " $($_.Text)" }
Write-Output ""
}
}
# ─── Search scoring ──────────────────────────────────────────────────────────
# Simple ranked scoring on type names. Higher = better.
# 100 = exact name 80 = starts-with 60 = substring
# 50 = PascalCase 40 = multi-keyword 20 = fuzzy subsequence
function Get-MatchScore {
param([string]$Name, [string]$FullName, [string]$Query)
$q = $Query.Trim()
if (-not $q) { return 0 }
if ($Name -eq $q) { return 100 }
if ($Name -like "$q*") { return 80 }
if ($Name -like "*$q*" -or $FullName -like "*$q*") { return 60 }
$initials = ($Name.ToCharArray() | Where-Object { [char]::IsUpper($_) }) -join ''
if ($initials.Length -ge 2 -and $initials -like "*$q*") { return 50 }
$words = $q -split '\s+' | Where-Object { $_.Length -gt 0 }
if ($words.Count -gt 1) {
$allFound = $true
foreach ($w in $words) {
if ($Name -notlike "*$w*" -and $FullName -notlike "*$w*") {
$allFound = $false
break
}
}
if ($allFound) { return 40 }
}
if (Test-FuzzySubsequence -Text $Name -Pattern $q) { return 20 }
return 0
}
function Test-FuzzySubsequence {
param([string]$Text, [string]$Pattern)
$ti = 0
$tLower = $Text.ToLowerInvariant()
$pLower = $Pattern.ToLowerInvariant()
foreach ($ch in $pLower.ToCharArray()) {
$idx = $tLower.IndexOf($ch, $ti)
if ($idx -lt 0) { return $false }
$ti = $idx + 1
}
return $true
}
# ─── Action: enums ───────────────────────────────────────────────────────────
function Get-EnumValues {
param([string]$FullName)
if (-not $FullName) {
Write-Error "-TypeName is required for 'enums' action."
exit 1
}
$lastDot = $FullName.LastIndexOf('.')
if ($lastDot -lt 1) {
Write-Error "-TypeName must be a fully-qualified type name including namespace, e.g. 'Namespace.TypeName'. Provided: $FullName"
exit 1
}
$ns = $FullName.Substring(0, $lastDot)
$safeFile = $ns.Replace('.', '_') + '.json'
$manifest = Resolve-ProjectManifest -Name $Project
$dirs = Get-PackageCacheDirs -Manifest $manifest
foreach ($dir in $dirs) {
$filePath = Join-Path $dir "types\$safeFile"
if (-not (Test-Path $filePath)) { continue }
$types = Get-Content $filePath -Raw | ConvertFrom-Json
$type = $types | Where-Object { $_.fullName -eq $FullName }
if (-not $type) { continue }
if ($type.kind -ne 'Enum') {
Write-Error "$FullName is not an Enum (kind: $($type.kind))"
exit 1
}
Write-Output "Enum $($type.fullName)"
if ($type.enumValues) {
$type.enumValues | ForEach-Object { Write-Output " $_" }
} else {
Write-Output " (no values)"
}
return
}
Write-Error "Type not found: $FullName"
exit 1
}
# ─── Dispatch ─────────────────────────────────────────────────────────────────
switch ($Action) {
'projects' { Show-Projects }
'packages' { Show-Packages }
'stats' { Show-Stats }
'namespaces' { Get-Namespaces -Prefix $Filter }
'types' { Get-TypesInNamespace -Ns $Namespace }
'members' { Get-MembersOfType -FullName $TypeName }
'search' { Search-WinMd -SearchQuery $Query -Max $MaxResults }
'enums' { Get-EnumValues -FullName $TypeName }
}

View File

@@ -0,0 +1,208 @@
<#
.SYNOPSIS
Generate or refresh the WinMD cache for the Agent Skill.
.DESCRIPTION
Builds and runs the standalone cache generator to export cached JSON files
from all WinMD metadata found in project NuGet packages and Windows SDK.
The cache is per-package+version: if two projects reference the same
package at the same version, the WinMD data is parsed once and shared.
Supports single project or recursive scan of an entire repo.
.PARAMETER ProjectDir
Path to a project directory (contains .csproj/.vcxproj), or a project file itself.
Defaults to scanning the workspace root.
.PARAMETER Scan
Recursively discover all .csproj/.vcxproj files under ProjectDir.
.PARAMETER OutputDir
Path to the cache output directory. Defaults to "Generated Files\winmd-cache".
.EXAMPLE
.\Update-WinMdCache.ps1
.\Update-WinMdCache.ps1 -ProjectDir BlankWinUI
.\Update-WinMdCache.ps1 -Scan -ProjectDir .
.\Update-WinMdCache.ps1 -ProjectDir "src\MyApp\MyApp.csproj"
#>
[CmdletBinding()]
param(
[string]$ProjectDir,
[switch]$Scan,
[string]$OutputDir = 'Generated Files\winmd-cache'
)
$ErrorActionPreference = 'Stop'
# Convention: skill lives at .github/skills/winmd-api-search/scripts/
# so workspace root is 4 levels up from $PSScriptRoot.
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')).Path
$generatorProj = Join-Path (Join-Path $PSScriptRoot 'cache-generator') 'CacheGenerator.csproj'
# ---------------------------------------------------------------------------
# WinAppSDK version detection -- look only at the repo root folder (no recursion)
# ---------------------------------------------------------------------------
function Get-WinAppSdkVersionFromDirectoryPackagesProps {
<#
.SYNOPSIS
Extract Microsoft.WindowsAppSDK version from a Directory.Packages.props
(Central Package Management) at the repo root.
#>
param([string]$RepoRoot)
$propsFile = Join-Path $RepoRoot 'Directory.Packages.props'
if (-not (Test-Path $propsFile)) { return $null }
try {
[xml]$xml = Get-Content $propsFile -Raw
$node = $xml.SelectNodes('//PackageVersion') |
Where-Object { $_.Include -eq 'Microsoft.WindowsAppSDK' } |
Select-Object -First 1
if ($node) { return $node.Version }
} catch {
Write-Verbose "Could not parse $propsFile : $_"
}
return $null
}
function Get-WinAppSdkVersionFromPackagesConfig {
<#
.SYNOPSIS
Extract Microsoft.WindowsAppSDK version from a packages.config at the repo root.
#>
param([string]$RepoRoot)
$configFile = Join-Path $RepoRoot 'packages.config'
if (-not (Test-Path $configFile)) { return $null }
try {
[xml]$xml = Get-Content $configFile -Raw
$node = $xml.SelectNodes('//package') |
Where-Object { $_.id -eq 'Microsoft.WindowsAppSDK' } |
Select-Object -First 1
if ($node) { return $node.version }
} catch {
Write-Verbose "Could not parse $configFile : $_"
}
return $null
}
# Try Directory.Packages.props first (CPM), then packages.config
$winAppSdkVersion = Get-WinAppSdkVersionFromDirectoryPackagesProps -RepoRoot $root
if (-not $winAppSdkVersion) {
$winAppSdkVersion = Get-WinAppSdkVersionFromPackagesConfig -RepoRoot $root
}
if ($winAppSdkVersion) {
Write-Host "Detected WinAppSDK version from repo: $winAppSdkVersion" -ForegroundColor Cyan
} else {
Write-Host "No WinAppSDK version found at repo root; will use latest (Version=*)" -ForegroundColor Yellow
}
# Default: if no ProjectDir, scan the workspace root
if (-not $ProjectDir) {
$ProjectDir = $root
$Scan = $true
}
Push-Location $root
try {
# Detect installed .NET SDK -- require >= 8.0, prefer stable over preview
$dotnetSdks = dotnet --list-sdks 2>$null
$bestMajor = $dotnetSdks |
Where-Object { $_ -notmatch 'preview|rc|alpha|beta' } |
ForEach-Object { if ($_ -match '^(\d+)\.') { [int]$Matches[1] } } |
Where-Object { $_ -ge 8 } |
Sort-Object -Descending |
Select-Object -First 1
# Fall back to preview SDKs if no stable SDK found
if (-not $bestMajor) {
$bestMajor = $dotnetSdks |
ForEach-Object { if ($_ -match '^(\d+)\.') { [int]$Matches[1] } } |
Where-Object { $_ -ge 8 } |
Sort-Object -Descending |
Select-Object -First 1
}
if (-not $bestMajor) {
Write-Error "No .NET SDK >= 8.0 found. Install from https://dotnet.microsoft.com/download"
exit 1
}
$targetFramework = "net$bestMajor.0"
Write-Host "Using .NET SDK: $targetFramework" -ForegroundColor Cyan
# Build MSBuild properties -- pass detected WinAppSDK version when available
$sdkVersionProp = ''
if ($winAppSdkVersion) {
$sdkVersionProp = "-p:WinAppSdkVersion=$winAppSdkVersion"
}
Write-Host "Building cache generator..." -ForegroundColor Cyan
$restoreArgs = @($generatorProj, "-p:TargetFramework=$targetFramework", '--nologo', '-v', 'q')
if ($sdkVersionProp) { $restoreArgs += $sdkVersionProp }
dotnet restore @restoreArgs
if ($LASTEXITCODE -ne 0) {
Write-Error "Restore failed"
exit 1
}
$buildArgs = @($generatorProj, '-c', 'Release', '--nologo', '-v', 'q', "-p:TargetFramework=$targetFramework", '--no-restore')
if ($sdkVersionProp) { $buildArgs += $sdkVersionProp }
dotnet build @buildArgs
if ($LASTEXITCODE -ne 0) {
Write-Error "Build failed"
exit 1
}
# Run the built executable directly (avoids dotnet run target framework mismatch issues)
$generatorDir = Join-Path $PSScriptRoot 'cache-generator'
$exePath = Join-Path $generatorDir "bin\Release\$targetFramework\CacheGenerator.exe"
if (-not (Test-Path $exePath)) {
# Fallback: try dll with dotnet
$dllPath = Join-Path $generatorDir "bin\Release\$targetFramework\CacheGenerator.dll"
if (Test-Path $dllPath) {
$exePath = $null
} else {
Write-Error "Built executable not found at: $exePath"
exit 1
}
}
$runArgs = @()
if ($Scan) {
$runArgs += '--scan'
}
# Detect installed WinAppSDK runtime via Get-AppxPackage (the WindowsApps
# folder is ACL-restricted so C# cannot enumerate it directly).
# WinMD files are architecture-independent metadata, so pick whichever arch
# matches the current OS to ensure the package is present.
$osArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
$runtimePkg = Get-AppxPackage -Name 'Microsoft.WindowsAppRuntime.*' -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notmatch 'CBS' -and $_.Architecture -eq $osArch } |
Sort-Object -Property Version -Descending |
Select-Object -First 1
if ($runtimePkg -and $runtimePkg.InstallLocation -and (Test-Path $runtimePkg.InstallLocation)) {
Write-Host "Detected WinAppSDK runtime: $($runtimePkg.Name) v$($runtimePkg.Version)" -ForegroundColor Cyan
$runArgs += '--winappsdk-runtime'
$runArgs += $runtimePkg.InstallLocation
}
$runArgs += $ProjectDir
$runArgs += $OutputDir
Write-Host "Exporting WinMD cache..." -ForegroundColor Cyan
if ($exePath) {
& $exePath @runArgs
} else {
dotnet $dllPath @runArgs
}
if ($LASTEXITCODE -ne 0) {
Write-Error "Cache export failed"
exit 1
}
Write-Host "Cache updated at: $OutputDir" -ForegroundColor Green
} finally {
Pop-Location
}

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<!-- Default fallback; Update-WinMdCache.ps1 overrides via -p:TargetFramework=net{X}.0 -->
<TargetFramework Condition="'$(TargetFramework)' == ''">net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<!-- System.Reflection.Metadata is inbox in net9.0+, only needed for net8.0 -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="System.Reflection.Metadata" Version="8.0.1" />
</ItemGroup>
<!--
Baseline WinAppSDK packages: downloaded during restore so the cache generator
can always index WinAppSDK APIs, even if the target project hasn't been restored.
ExcludeAssets="all" means they're downloaded but don't affect this tool's build.
When the repo has a known version (passed via -p:WinAppSdkVersion=X.Y.Z from
Update-WinMdCache.ps1), prefer that version to avoid unnecessary NuGet downloads.
Falls back to Version="*" (latest) on fresh clones with no restore.
-->
<ItemGroup Condition="'$(WinAppSdkVersion)' != ''">
<PackageReference Include="Microsoft.WindowsAppSDK" Version="$(WinAppSdkVersion)" ExcludeAssets="all" />
</ItemGroup>
<ItemGroup Condition="'$(WinAppSdkVersion)' == ''">
<PackageReference Include="Microsoft.WindowsAppSDK" Version="*" ExcludeAssets="all" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,3 @@
<Project>
<!-- Isolate this standalone tool from the repo-level build configuration -->
</Project>

View File

@@ -0,0 +1,3 @@
<Project>
<!-- Isolate this standalone tool from the repo-level build targets -->
</Project>

View File

@@ -0,0 +1,3 @@
<Project>
<!-- Isolate this standalone tool from the repo-level Central Package Management -->
</Project>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
---
name: wpf-to-winui3-migration
description: Guide for migrating PowerToys modules from WPF to WinUI 3 (Windows App SDK). Use when asked to migrate WPF code, convert WPF XAML to WinUI, replace System.Windows namespaces with Microsoft.UI.Xaml, update Dispatcher to DispatcherQueue, replace DynamicResource with ThemeResource, migrate imaging APIs from System.Windows.Media.Imaging to Windows.Graphics.Imaging, convert WPF Window to WinUI Window, migrate .resx to .resw resources, migrate custom Observable/RelayCommand to CommunityToolkit.Mvvm source generators, handle WPF-UI (Lepo) to WinUI native control migration, or fix installer/build pipeline issues after migration. Keywords: WPF, WinUI, WinUI3, migration, porting, convert, namespace, XAML, Dispatcher, DispatcherQueue, imaging, BitmapImage, Window, ContentDialog, ThemeResource, DynamicResource, ResourceLoader, resw, resx, CommunityToolkit, ObservableProperty, WPF-UI, SizeToContent, AppWindow, SoftwareBitmap.
license: Complete terms in LICENSE.txt
---
# WPF to WinUI 3 Migration Skill
Migrate PowerToys modules from WPF (`System.Windows.*`) to WinUI 3 (`Microsoft.UI.Xaml.*` / Windows App SDK). Based on patterns validated in the ImageResizer module migration.
## When to Use This Skill
- Migrate a PowerToys module from WPF to WinUI 3
- Convert WPF XAML files to WinUI 3 XAML
- Replace `System.Windows` namespaces with `Microsoft.UI.Xaml`
- Migrate `Dispatcher` usage to `DispatcherQueue`
- Migrate custom `Observable`/`RelayCommand` to CommunityToolkit.Mvvm source generators
- Replace WPF-UI (Lepo) controls with native WinUI 3 controls
- Convert imaging code from `System.Windows.Media.Imaging` to `Windows.Graphics.Imaging`
- Handle WPF `Window` vs WinUI `Window` differences (sizing, positioning, SizeToContent)
- Migrate resource files from `.resx` to `.resw` with `ResourceLoader`
- Fix installer/build pipeline issues after WinUI 3 migration
- Update project files, NuGet packages, and signing config
## Prerequisites
- Visual Studio 2022 17.4+
- Windows App SDK NuGet package (`Microsoft.WindowsAppSDK`)
- .NET 8+ with `net8.0-windows10.0.19041.0` TFM
- Windows 10 1803+ (April 2018 Update or newer)
## Migration Strategy
### Recommended Order
1. **Project file** — Update TFM, NuGet packages, set `<UseWinUI>true</UseWinUI>`
2. **Data models and business logic** — No UI dependencies, migrate first
3. **MVVM framework** — Replace custom Observable/RelayCommand with CommunityToolkit.Mvvm
4. **Resource strings** — Migrate `.resx``.resw`, introduce `ResourceLoaderInstance`
5. **Services and utilities** — Replace `System.Windows` types, async-ify imaging code
6. **ViewModels** — Update Dispatcher usage, binding patterns
7. **Views/Pages** — Starting from leaf pages with fewest dependencies
8. **Main page / shell** — Last, since it depends on everything
9. **App.xaml / startup code** — Merge carefully (do NOT overwrite WinUI 3 boilerplate)
10. **Installer & build pipeline** — Update WiX, signing, build events
11. **Tests** — Adapt for WinUI 3 runtime, async patterns
### Key Principles
- **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.
## 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` |
### Critical API Replacements
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| `Dispatcher.Invoke()` | `DispatcherQueue.TryEnqueue()` | Different return type (`bool`) |
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` | Property vs method |
| `Application.Current.Dispatcher` | Store `DispatcherQueue` in static field | See [Threading](./references/threading-and-windowing.md) |
| `MessageBox.Show()` | `ContentDialog` | Must set `XamlRoot` |
| `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 |
| `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 |
### 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` |
### 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 |
## Detailed Reference Docs
Read only the section relevant to your current task:
- [Namespace and API Mapping](./references/namespace-api-mapping.md) — Full type mapping, NuGet changes, project file, CsWinRT interop
- [XAML Migration Guide](./references/xaml-migration.md) — XAML syntax, WPF-UI removal, markup extensions, styles, resources, data binding
- [Threading and Window Management](./references/threading-and-windowing.md) — Dispatcher, DispatcherQueue, SizeToContent, AppWindow, HWND interop, custom entry point
- [Imaging API Migration](./references/imaging-migration.md) — BitmapEncoder/Decoder, SoftwareBitmap, CodecHelper, async patterns, int→uint
- [PowerToys-Specific Patterns](./references/powertoys-patterns.md) — MVVM migration, ResourceLoader, Lazy init, installer, signing, test adaptation, build pipeline
## Common Pitfalls (from ImageResizer migration)
| Pitfall | Solution |
|---------|----------|
| `ContentDialog` throws "does not have a XamlRoot" | Set `dialog.XamlRoot = this.Content.XamlRoot` before `ShowAsync()` |
| `FilePicker` throws error in desktop app | Call `WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd)` |
| `Window.Dispatcher` returns null | Use `Window.DispatcherQueue` instead |
| Resources on `Window` element not found | Move resources to root layout container (`Grid.Resources`) |
| `VisualStateManager` on `Window` fails | Use `UserControl` or `Page` inside the Window |
| Satellite assembly installer errors (`WIX0103`) | Remove `.resources.dll` refs from `Resources.wxs`; WinUI 3 uses `.pri` |
| Phantom `.exe`/`.deps.json` in root output dir | Avoid Exe→WinExe `ProjectReference`; use Library project |
| `ResourceLoader` crash at static init | Wrap in `Lazy<T>` or null-coalescing property — see [Lazy Init](./references/powertoys-patterns.md#lazy-initialization-for-resource-dependent-statics) |
| `SizeToContent` not available | Implement manual content measurement + `AppWindow.Resize()` with DPI scaling |
| `x:Bind` default mode is `OneTime` | Explicitly set `Mode=OneWay` or `Mode=TwoWay` |
| `DynamicResource` / `x:Static` not compiling | Replace with `ThemeResource` / `ResourceLoader` or `x:Uid` |
| `IValueConverter.Convert` signature mismatch | Last param: `CultureInfo``string` (language tag) |
| Test project can't resolve WPF types | Add `<UseWPF>true</UseWPF>` temporarily; remove after imaging migration |
| Pixel dimension type mismatch (`int` vs `uint`) | WinRT uses `uint` for pixel sizes — add `u` suffix in test assertions |
| `$(SolutionDir)` empty in standalone project build | Use `$(MSBuildThisFileDirectory)` with relative paths instead |
| 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` |
## Troubleshooting
| Issue | Solution |
|-------|----------|
| Build fails after namespace rename | Check for lingering `System.Windows` usings; some types have no direct equivalent |
| Missing `PresentationCore.dll` at runtime | Ensure ALL imaging code uses `Windows.Graphics.Imaging`, not `System.Windows.Media.Imaging` |
| `DataContext` not working on Window | WinUI 3 `Window` is not a `DependencyObject`; use a root `Page` or `UserControl` |
| XAML designer not available | WinUI 3 does not support XAML Designer; use Hot Reload instead |
| 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 |

View File

@@ -0,0 +1,287 @@
# Imaging API Migration
Migrating from WPF (`System.Windows.Media.Imaging` / `PresentationCore.dll`) to WinRT (`Windows.Graphics.Imaging`). Based on the ImageResizer migration.
## Why This Migration Is Required
WinUI 3 apps deployed as self-contained do NOT include `PresentationCore.dll`. Any code using `System.Windows.Media.Imaging` will throw `FileNotFoundException` at runtime. ALL imaging code must use WinRT APIs.
| Purpose | Namespace |
|---------|-----------|
| UI display (`Image.Source`) | `Microsoft.UI.Xaml.Media.Imaging` |
| Image processing (encode/decode/transform) | `Windows.Graphics.Imaging` |
## Architecture Change: Pipeline vs Declarative
The fundamental architecture differs:
**WPF**: In-memory pipeline of bitmap objects. Decode → transform → encode synchronously.
```csharp
var decoder = BitmapDecoder.Create(stream, ...);
var transform = new TransformedBitmap(decoder.Frames[0], new ScaleTransform(...));
var encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(transform, ...));
encoder.Save(outputStream);
```
**WinRT**: Declarative transform model. Configure transforms on the encoder, which handles pixel manipulation internally. All async.
```csharp
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
encoder.BitmapTransform.ScaledWidth = newWidth;
encoder.BitmapTransform.ScaledHeight = newHeight;
encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
await encoder.FlushAsync();
```
## Core Type Mapping
### Decoders
| WPF | WinRT | Notes |
|-----|-------|-------|
| `BitmapDecoder.Create(stream, options, cache)` | `BitmapDecoder.CreateAsync(stream)` | Async, auto-detects format |
| `JpegBitmapDecoder` / `PngBitmapDecoder` / etc. | `BitmapDecoder.CreateAsync(stream)` | Single unified decoder |
| `decoder.Frames[0]` | `await decoder.GetFrameAsync(0)` | Async frame access |
| `decoder.Frames.Count` | `decoder.FrameCount` (uint) | `int``uint` |
| `decoder.CodecInfo.ContainerFormat` | `decoder.DecoderInformation.CodecId` | Different property path |
| `decoder.Frames[0].PixelWidth` (int) | `decoder.PixelWidth` (uint) | `int``uint` |
| `WmpBitmapDecoder` | Not available | WMP/HDP not supported |
### Encoders
| WPF | WinRT | Notes |
|-----|-------|-------|
| `new JpegBitmapEncoder()` | `BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream)` | Async factory |
| `new PngBitmapEncoder()` | `BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream)` | No interlace control |
| `encoder.Frames.Add(frame)` | `encoder.SetSoftwareBitmap(bitmap)` | Different API |
| `encoder.Save(stream)` | `await encoder.FlushAsync()` | Async |
### Encoder Properties (Strongly-Typed → BitmapPropertySet)
WPF had type-specific encoder subclasses. WinRT uses a generic property set:
```csharp
// WPF
case JpegBitmapEncoder jpeg: jpeg.QualityLevel = 85; // int 1-100
case PngBitmapEncoder png: png.Interlace = PngInterlaceOption.On;
case TiffBitmapEncoder tiff: tiff.Compression = TiffCompressOption.Lzw;
// WinRT — JPEG quality (float 0.0-1.0)
await encoder.BitmapProperties.SetPropertiesAsync(new BitmapPropertySet
{
{ "ImageQuality", new BitmapTypedValue(0.85f, PropertyType.Single) }
});
// WinRT — TIFF compression (via BitmapPropertySet at creation time)
var props = new BitmapPropertySet
{
{ "TiffCompressionMethod", new BitmapTypedValue((byte)2, PropertyType.UInt8) }
};
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.TiffEncoderId, stream, props);
```
**JPEG quality scale change**: WPF int `1-100` → WinRT float `0.0-1.0`. Divide by 100.
### Bitmap Types
| WPF | WinRT | Notes |
|-----|-------|-------|
| `BitmapSource` | `SoftwareBitmap` | Central pixel-data type |
| `BitmapImage` | `BitmapImage` (in `Microsoft.UI.Xaml.Media.Imaging`) | UI display only |
| `FormatConvertedBitmap` | `SoftwareBitmap.Convert()` | |
| `TransformedBitmap` + `ScaleTransform` | `BitmapTransform` via encoder | Declarative |
| `CroppedBitmap` | `BitmapTransform.Bounds` | |
### Metadata
| WPF | WinRT | Notes |
|-----|-------|-------|
| `BitmapMetadata` | `BitmapProperties` | Different API surface |
| `BitmapMetadata.Clone()` | No equivalent | Cannot selectively clone |
| Selective metadata removal | Not supported | All-or-nothing only |
**Two encoder creation strategies for metadata:**
- `CreateForTranscodingAsync()` — preserves ALL metadata from source
- `CreateAsync()` — creates fresh encoder with NO metadata
This eliminated ~258 lines of manual metadata manipulation code (`BitmapMetadataExtension.cs`) in ImageResizer.
### Interpolation Modes
| WPF `BitmapScalingMode` | WinRT `BitmapInterpolationMode` |
|------------------------|-------------------------------|
| `HighQuality` / `Fant` | `Fant` |
| `Linear` | `Linear` |
| `NearestNeighbor` | `NearestNeighbor` |
| `Unspecified` / `LowQuality` | `Linear` |
## Stream Interop
WinRT imaging requires `IRandomAccessStream` instead of `System.IO.Stream`:
```csharp
using var stream = File.OpenRead(path);
var winrtStream = stream.AsRandomAccessStream(); // Extension method
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
```
**Critical**: For transcode, seek the input stream back to 0 before creating the encoder:
```csharp
winrtStream.Seek(0);
var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
```
## CodecHelper Pattern (from ImageResizer)
WPF stored container format GUIDs in `settings.json`. WinRT uses different codec IDs. Create a `CodecHelper` to bridge them:
```csharp
internal static class CodecHelper
{
// Maps WPF container format GUIDs (stored in settings JSON) to WinRT encoder IDs
private static readonly Dictionary<Guid, Guid> LegacyGuidToEncoderId = new()
{
[new Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057")] = BitmapEncoder.JpegEncoderId,
[new Guid("1b7cfaf4-713f-473c-bbcd-6137425faeaf")] = BitmapEncoder.PngEncoderId,
[new Guid("0af1d87e-fcfe-4188-bdeb-a7906471cbe3")] = BitmapEncoder.BmpEncoderId,
[new Guid("163bcc30-e2e9-4f0b-961d-a3e9fdb788a3")] = BitmapEncoder.TiffEncoderId,
[new Guid("1f8a5601-7d4d-4cbd-9c82-1bc8d4eeb9a5")] = BitmapEncoder.GifEncoderId,
};
// Maps decoder IDs to corresponding encoder IDs
private static readonly Dictionary<Guid, Guid> DecoderIdToEncoderId = new()
{
[BitmapDecoder.JpegDecoderId] = BitmapEncoder.JpegEncoderId,
[BitmapDecoder.PngDecoderId] = BitmapEncoder.PngEncoderId,
// ...
};
public static Guid GetEncoderIdFromLegacyGuid(Guid legacyGuid)
=> LegacyGuidToEncoderId.GetValueOrDefault(legacyGuid, Guid.Empty);
public static Guid GetEncoderIdForDecoder(BitmapDecoder decoder)
=> DecoderIdToEncoderId.GetValueOrDefault(decoder.DecoderInformation.CodecId, Guid.Empty);
}
```
This preserves backward compatibility with existing `settings.json` files that contain WPF-era GUIDs.
## ImagingEnums Pattern (from ImageResizer)
WPF-specific enums (`PngInterlaceOption`, `TiffCompressOption`) from `System.Windows.Media.Imaging` are used in settings JSON. Create custom enums with identical integer values for backward-compatible deserialization:
```csharp
// Replace System.Windows.Media.Imaging.PngInterlaceOption
public enum PngInterlaceOption { Default = 0, On = 1, Off = 2 }
// Replace System.Windows.Media.Imaging.TiffCompressOption
public enum TiffCompressOption { Default = 0, None = 1, Ccitt3 = 2, Ccitt4 = 3, Lzw = 4, Rle = 5, Zip = 6 }
```
## Async Migration Patterns
### Method Signatures
All imaging operations become async:
| Before | After |
|--------|-------|
| `void Execute(file, settings)` | `async Task ExecuteAsync(file, settings)` |
| `IEnumerable<Error> Process()` | `async Task<IEnumerable<Error>> ProcessAsync()` |
### Parallel Processing
```csharp
// WPF (synchronous)
Parallel.ForEach(Files, new ParallelOptions { MaxDegreeOfParallelism = ... },
(file, state, i) => { Execute(file, settings); });
// WinRT (async)
await Parallel.ForEachAsync(Files, new ParallelOptions { MaxDegreeOfParallelism = ... },
async (file, ct) => { await ExecuteAsync(file, settings); });
```
### CLI Async Bridge
CLI entry points must bridge async to sync:
```csharp
return RunSilentModeAsync(cliOptions).GetAwaiter().GetResult();
```
### Task.Factory.StartNew → Task.Run
```csharp
// WPF
_ = Task.Factory.StartNew(StartExecutingWork, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
// WinUI 3
_ = Task.Run(() => StartExecutingWorkAsync());
```
## SoftwareBitmap as Interface Type
When modules expose imaging interfaces (e.g., AI super-resolution), change parameter/return types:
```csharp
// WPF
BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
// WinRT
SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath);
```
This eliminates manual `BitmapSource ↔ SoftwareBitmap` conversion code (unsafe `IMemoryBufferByteAccess` COM interop).
## MultiFrame Image Handling
```csharp
// WinRT multi-frame encode (e.g., multi-page TIFF, animated GIF)
for (uint i = 0; i < decoder.FrameCount; i++)
{
if (i > 0)
await encoder.GoToNextFrameAsync();
var frame = await decoder.GetFrameAsync(i);
var bitmap = await frame.GetSoftwareBitmapAsync(
frame.BitmapPixelFormat,
BitmapAlphaMode.Premultiplied,
transform,
ExifOrientationMode.IgnoreExifOrientation,
ColorManagementMode.DoNotColorManage);
encoder.SetSoftwareBitmap(bitmap);
}
await encoder.FlushAsync();
```
## int → uint for Pixel Dimensions
WinRT uses `uint` for all pixel dimensions. This affects:
- `decoder.PixelWidth` / `decoder.PixelHeight``uint`
- `BitmapTransform.ScaledWidth` / `ScaledHeight``uint`
- `SoftwareBitmap` constructor — `uint` parameters
- Test assertions: `Assert.AreEqual(96, ...)``Assert.AreEqual(96u, ...)`
## Display SoftwareBitmap in UI
```csharp
var source = new SoftwareBitmapSource();
// Must convert to Bgra8/Premultiplied for display
if (bitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
bitmap.BitmapAlphaMode != BitmapAlphaMode.Premultiplied)
{
bitmap = SoftwareBitmap.Convert(bitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}
await source.SetBitmapAsync(bitmap);
myImage.Source = source;
```
## Known Limitations
| Feature | WPF | WinRT | Impact |
|---------|-----|-------|--------|
| PNG interlace | `PngBitmapEncoder.Interlace` | Not available | Always non-interlaced |
| Metadata stripping | Selective via `BitmapMetadata.Clone()` | All-or-nothing | Orientation EXIF also removed |
| Pixel formats | Many (`Pbgra32`, `Bgr24`, `Indexed8`, ...) | Primarily `Bgra8`, `Rgba8`, `Gray8/16` | Convert to `Bgra8` |
| WMP/HDP format | `WmpBitmapDecoder` | Not available | Not supported |
| Pixel differences | WPF scaler | `BitmapInterpolationMode.Fant` | Not bit-identical |

View File

@@ -0,0 +1,226 @@
# Namespace and API Mapping Reference
Complete reference for mapping WPF types to WinUI 3 equivalents, based on the ImageResizer migration.
## 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` |
## Core Type Mapping
| WPF Type | WinUI 3 Type |
|----------|-------------|
| `System.Windows.Application` | `Microsoft.UI.Xaml.Application` |
| `System.Windows.Window` | `Microsoft.UI.Xaml.Window` (NOT a DependencyObject) |
| `System.Windows.DependencyObject` | `Microsoft.UI.Xaml.DependencyObject` |
| `System.Windows.DependencyProperty` | `Microsoft.UI.Xaml.DependencyProperty` |
| `System.Windows.FrameworkElement` | `Microsoft.UI.Xaml.FrameworkElement` |
| `System.Windows.UIElement` | `Microsoft.UI.Xaml.UIElement` |
| `System.Windows.Visibility` | `Microsoft.UI.Xaml.Visibility` |
| `System.Windows.Thickness` | `Microsoft.UI.Xaml.Thickness` |
| `System.Windows.CornerRadius` | `Microsoft.UI.Xaml.CornerRadius` |
| `System.Windows.Media.Color` | `Windows.UI.Color` (note: `Windows.UI`, not `Microsoft.UI`) |
| `System.Windows.Media.Colors` | `Microsoft.UI.Colors` |
## Controls Mapping
### Direct Mapping (namespace-only change)
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`
### Controls With Different Names or Behavior
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| `MessageBox` | `ContentDialog` | Must set `XamlRoot` before `ShowAsync()` |
| `ContextMenu` | `MenuFlyout` | Different API surface |
| `TabControl` | `TabView` | Different API |
| `Menu` | `MenuBar` | Different API |
| `StatusBar` | Custom `StackPanel` layout | No built-in equivalent |
| `AccessText` | Not available | Use `AccessKey` property on target control |
### WPF-UI (Lepo) to Native WinUI 3
ImageResizer used the `WPF-UI` library (Lepo) for Fluent styling. These must be replaced with native WinUI 3 equivalents:
| WPF-UI (Lepo) | WinUI 3 Native | Notes |
|----------------|---------------|-------|
| `<ui:FluentWindow>` | `<Window>` | Native window + `ExtendsContentIntoTitleBar` |
| `<ui:Button>` | `<Button>` | Native button |
| `<ui:NumberBox>` | `<NumberBox>` | Built into WinUI 3 |
| `<ui:ProgressRing>` | `<ProgressRing>` | Built into WinUI 3 |
| `<ui:SymbolIcon>` | `<SymbolIcon>` or `<FontIcon>` | Built into WinUI 3 |
| `<ui:InfoBar>` | `<InfoBar>` | Built into WinUI 3 |
| `<ui:TitleBar>` | Custom title bar via `SetTitleBar()` | Use `ExtendsContentIntoTitleBar` |
| `<ui:ThemesDictionary>` | `<XamlControlsResources>` | In merged dictionaries |
| `<ui:ControlsDictionary>` | Remove | Not needed — WinUI 3 has its own control styles |
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` | Named style keys |
## Input Event Mapping
| 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` | Uses `PointerRoutedEventArgs` |
| `MouseWheel` | `PointerWheelChanged` | Different event args |
| `MouseEnter` | `PointerEntered` | |
| `MouseLeave` | `PointerExited` | |
| `MouseDoubleClick` | `DoubleTapped` | Different event args |
| `KeyDown` | `KeyDown` | Same name, args type: `KeyRoutedEventArgs` |
| `PreviewKeyDown` | No direct equivalent | Use `KeyDown` with handled pattern |
## IValueConverter Signature Change
| WPF | WinUI 3 |
|-----|---------|
| `Convert(object value, Type targetType, object parameter, CultureInfo culture)` | `Convert(object value, Type targetType, object parameter, string language)` |
| `ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)` | `ConvertBack(object value, Type targetType, object parameter, string language)` |
Last parameter changes from `CultureInfo` to `string` (BCP-47 language tag). All converter classes must be updated.
## Types That Moved to Different Hierarchies
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| `System.Windows.Threading.Dispatcher` | `Microsoft.UI.Dispatching.DispatcherQueue` | Completely different API |
| `System.Windows.Threading.DispatcherPriority` | `Microsoft.UI.Dispatching.DispatcherQueuePriority` | Only 3 levels: High/Normal/Low |
| `System.Windows.Interop.HwndSource` | `WinRT.Interop.WindowNative` | For HWND interop |
| `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 |
## NuGet Package Migration
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| Built into .NET (no NuGet needed) | `Microsoft.WindowsAppSDK` | Required |
| `PresentationCore` / `PresentationFramework` | `Microsoft.WinUI` (transitive) | |
| `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.Windows.SDK.BuildTools` | Required |
| (none) | `WinUIEx` | Optional, window helpers |
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
| (none) | `CommunityToolkit.WinUI.Extensions` | Optional |
| (none) | `Microsoft.Web.WebView2` | If using WebView |
## Project File Changes
### WPF .csproj
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
</Project>
```
### WinUI 3 .csproj
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<UseWinUI>true</UseWinUI>
<SelfContained>true</SelfContained>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<WindowsPackageType>None</WindowsPackageType>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets\ImageResizer\ImageResizer.ico</ApplicationIcon>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
<ProjectPriFileName>PowerToys.ModuleName.pri</ProjectPriFileName>
</PropertyGroup>
</Project>
```
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`
- 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>/`
### XAML ApplicationDefinition Setup
WinUI 3 requires explicit `ApplicationDefinition` declaration:
```xml
<ItemGroup>
<Page Remove="ImageResizerXAML\App.xaml" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="ImageResizerXAML\App.xaml" />
</ItemGroup>
```
### CsWinRT Interop (for GPO and native references)
If the module references native C++ projects (like `GPOWrapper`):
```xml
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
</PropertyGroup>
```
Change `GPOWrapperProjection.csproj` reference to direct `GPOWrapper.vcxproj` reference.
### InternalsVisibleTo Migration
Move from code file to `.csproj`:
```csharp
// DELETE: Properties/InternalsVisibleTo.cs
// [assembly: InternalsVisibleTo("ImageResizer.Test")]
```
```xml
<!-- ADD to .csproj: -->
<ItemGroup>
<InternalsVisibleTo Include="ImageResizer.Test" />
</ItemGroup>
```
### Items to Remove from .csproj
```xml
<!-- DELETE: WPF resource embedding -->
<EmbeddedResource Update="Properties\Resources.resx">...</EmbeddedResource>
<Resource Include="Resources\ImageResizer.ico" />
<Compile Update="Properties\Resources.Designer.cs">...</Compile>
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" /> <!-- from CLI project -->
```

View File

@@ -0,0 +1,516 @@
# PowerToys-Specific Migration Patterns
Patterns and conventions specific to the PowerToys codebase, based on the ImageResizer migration.
## Project Structure
### Before (WPF Module)
```
src/modules/<module>/
├── <Module>UI/
│ ├── <Module>UI.csproj # OutputType=WinExe, UseWPF=true
│ ├── App.xaml / App.xaml.cs
│ ├── MainWindow.xaml / .cs
│ ├── Views/
│ ├── ViewModels/
│ ├── Helpers/
│ │ ├── Observable.cs # Custom INotifyPropertyChanged
│ │ └── RelayCommand.cs # Custom ICommand
│ ├── Properties/
│ │ ├── Resources.resx # WPF resource strings
│ │ ├── Resources.Designer.cs
│ │ └── InternalsVisibleTo.cs
│ └── Telemetry/
├── <Module>CLI/
│ └── <Module>CLI.csproj # OutputType=Exe
└── tests/
```
### After (WinUI 3 Module)
```
src/modules/<module>/
├── <Module>UI/
│ ├── <Module>UI.csproj # OutputType=WinExe, UseWinUI=true
│ ├── Program.cs # Custom entry point (DISABLE_XAML_GENERATED_MAIN)
│ ├── app.manifest # Single manifest file
│ ├── ImageResizerXAML/
│ │ ├── App.xaml / App.xaml.cs # WinUI 3 App class
│ │ ├── MainWindow.xaml / .cs
│ │ └── Views/
│ ├── Converters/ # WinUI 3 IValueConverter (string language)
│ ├── ViewModels/
│ ├── Helpers/
│ │ └── ResourceLoaderInstance.cs # Static ResourceLoader accessor
│ ├── Utilities/
│ │ └── CodecHelper.cs # WPF→WinRT codec ID mapping (if imaging)
│ ├── Models/
│ │ └── ImagingEnums.cs # Custom enums replacing WPF imaging enums
│ ├── Strings/
│ │ └── en-us/
│ │ └── Resources.resw # WinUI 3 resource strings
│ └── Assets/
│ └── <Module>/
│ └── <Module>.ico # Moved from Resources/
├── <Module>Common/ # NEW: shared library for CLI
│ └── <Module>Common.csproj # OutputType=Library
├── <Module>CLI/
│ └── <Module>CLI.csproj # References Common, NOT UI
└── tests/
```
### Critical: CLI Dependency Pattern
**Do NOT** create `ProjectReference` from Exe to WinExe. This causes phantom build artifacts (`.exe`, `.deps.json`, `.runtimeconfig.json`) in the root output directory.
```
WRONG: ImageResizerCLI (Exe) → ImageResizerUI (WinExe) ← phantom artifacts
CORRECT: ImageResizerCLI (Exe) → ImageResizerCommon (Library)
ImageResizerUI (WinExe) → ImageResizerCommon (Library)
```
Follow the `FancyZonesCLI``FancyZonesEditorCommon` pattern.
### Files to Delete
| File | Reason |
|------|--------|
| `Properties/Resources.resx` | Replaced by `Strings/en-us/Resources.resw` |
| `Properties/Resources.Designer.cs` | Auto-generated; no longer needed |
| `Properties/InternalsVisibleTo.cs` | Moved to `.csproj` `<InternalsVisibleTo>` |
| `Helpers/Observable.cs` | Replaced by `CommunityToolkit.Mvvm.ObservableObject` |
| `Helpers/RelayCommand.cs` | Replaced by `CommunityToolkit.Mvvm.Input` |
| `Resources/*.ico` / `Resources/*.png` | Moved to `Assets/<Module>/` |
| WPF `.dev.manifest` / `.prod.manifest` | Replaced by single `app.manifest` |
| WPF-specific converters | Replaced by WinUI 3 converters with `string language` |
---
## MVVM Migration: Custom → CommunityToolkit.Mvvm Source Generators
### Observable Base Class → ObservableObject + [ObservableProperty]
**Before (custom Observable):**
```csharp
public class ResizeSize : Observable
{
private int _id;
public int Id { get => _id; set => Set(ref _id, value); }
private ResizeFit _fit;
public ResizeFit Fit
{
get => _fit;
set
{
Set(ref _fit, value);
UpdateShowHeight();
}
}
private bool _showHeight = true;
public bool ShowHeight { get => _showHeight; set => Set(ref _showHeight, value); }
private void UpdateShowHeight() { ShowHeight = Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent; }
}
```
**After (CommunityToolkit.Mvvm source generators):**
```csharp
public partial class ResizeSize : ObservableObject // MUST be partial
{
[ObservableProperty]
[JsonPropertyName("Id")]
private int _id;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowHeight))] // Replaces manual UpdateShowHeight()
private ResizeFit _fit;
// Computed property — no backing field, no manual update method
public bool ShowHeight => Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent;
}
```
Key changes:
- Class must be `partial` for source generators
- `Observable``ObservableObject` (from CommunityToolkit.Mvvm)
- Manual `Set(ref _field, value)``[ObservableProperty]` attribute
- `PropertyChanged` dependencies → `[NotifyPropertyChangedFor(nameof(...))]`
- Computed properties with manual `UpdateXxx()` → direct expression body
### Custom Name Setter with Transform
For properties that transform the value before storing:
```csharp
// Cannot use [ObservableProperty] because of value transformation
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, ReplaceTokens(value)); // SetProperty from ObservableObject
}
```
### RelayCommand → [RelayCommand] Source Generator
```csharp
// DELETE: Helpers/RelayCommand.cs (custom ICommand)
// Before
public ICommand ResizeCommand { get; } = new RelayCommand(Execute);
// After
[RelayCommand]
private void Resize() { /* ... */ }
// Source generator creates ResizeCommand property automatically
```
---
## Resource String Migration (.resx → .resw)
### ResourceLoaderInstance Helper
```csharp
internal static class ResourceLoaderInstance
{
internal static ResourceLoader ResourceLoader { get; private set; }
static ResourceLoaderInstance()
{
ResourceLoader = new ResourceLoader("PowerToys.ImageResizer.pri");
}
}
```
**Note**: Use the single-argument `ResourceLoader` constructor. The two-argument version (`ResourceLoader("file.pri", "path/Resources")`) may fail if the resource map path doesn't match the actual PRI structure.
### Usage
```csharp
// WPF
using ImageResizer.Properties;
string text = Resources.MyStringKey;
// WinUI 3
string text = ResourceLoaderInstance.ResourceLoader.GetString("MyStringKey");
```
### Lazy Initialization for Resource-Dependent Statics
`ResourceLoader` is not available at class-load time in all contexts (CLI mode, test harness). Use lazy initialization:
**Before (crashes at class load):**
```csharp
private static readonly CompositeFormat _format =
CompositeFormat.Parse(Resources.Error_Format);
private static readonly Dictionary<string, string> _tokens = new()
{
["$small$"] = Resources.Small,
["$medium$"] = Resources.Medium,
};
```
**After (lazy, safe):**
```csharp
private static CompositeFormat _format;
private static CompositeFormat Format => _format ??=
CompositeFormat.Parse(ResourceLoaderInstance.ResourceLoader.GetString("Error_Format"));
private static readonly Lazy<Dictionary<string, string>> _tokens = new(() =>
new Dictionary<string, string>
{
["$small$"] = ResourceLoaderInstance.ResourceLoader.GetString("Small"),
["$medium$"] = ResourceLoaderInstance.ResourceLoader.GetString("Medium"),
});
// Usage: _tokens.Value.TryGetValue(...)
```
### XAML: x:Static → x:Uid
```xml
<!-- WPF -->
<Button Content="{x:Static p:Resources.Cancel}" />
<!-- WinUI 3 -->
<Button x:Uid="Cancel" />
```
In `.resw`, use property-suffixed keys: `Cancel.Content`, `Header.Text`, etc.
---
## CLI Options Migration
`System.CommandLine.Option<T>` constructor signature changed:
```csharp
// WPF era — string[] aliases
public DestinationOption()
: base(_aliases, Properties.Resources.CLI_Option_Destination)
// WinUI 3 — single string name
public DestinationOption()
: base(_aliases[0], ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Destination"))
```
---
## Installer Updates
### WiX Changes
#### 1. Remove Satellite Assembly References
Remove from `installer/PowerToysSetupVNext/Resources.wxs`:
- `<Component>` entries for `<Module>.resources.dll`
- `<RemoveFolder>` entries for locale directories
- Module from `WinUI3AppsInstallFolder` `ParentDirectory` loop
#### 2. Update File Component Generation
Run `generateAllFileComponents.ps1` after migration. For Exe→WinExe dependency issues, add cleanup logic:
```powershell
# Strip phantom ImageResizer files from BaseApplications.wxs
$content = $content -replace 'PowerToys\.ImageResizer\.exe', ''
$content = $content -replace 'PowerToys\.ImageResizer\.deps\.json', ''
$content = $content -replace 'PowerToys\.ImageResizer\.runtimeconfig\.json', ''
```
#### 3. Output Directory
WinUI 3 modules output to `WinUI3Apps/`:
```xml
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
```
### ESRP Signing
Update `.pipelines/ESRPSigning_core.json` — all module binaries must use `WinUI3Apps\\` paths:
```json
{
"FileList": [
"WinUI3Apps\\PowerToys.ImageResizer.exe",
"WinUI3Apps\\PowerToys.ImageResizerExt.dll",
"WinUI3Apps\\PowerToys.ImageResizerContextMenu.dll"
]
}
```
---
## Build Pipeline Fixes
### $(SolutionDir) → $(MSBuildThisFileDirectory)
`$(SolutionDir)` is empty when building individual projects outside the solution. Replace with relative paths from the project file:
```xml
<!-- Before (breaks on standalone project build) -->
<Exec Command="powershell $(SolutionDir)tools\build\convert-resx-to-rc.ps1" />
<!-- After (works always) -->
<Exec Command="powershell $(MSBuildThisFileDirectory)..\..\..\..\tools\build\convert-resx-to-rc.ps1" />
```
### MSIX Packaging: PreBuild → PostBuild
MSIX packaging must happen AFTER the build (artifacts not ready at PreBuild):
```xml
<!-- Before -->
<PreBuildEvent>MakeAppx.exe pack /d . /p "$(OutDir)Package.msix" /o</PreBuildEvent>
<!-- After -->
<PostBuildEvent>
if exist "$(OutDir)Package.msix" del "$(OutDir)Package.msix"
MakeAppx.exe pack /d "$(MSBuildThisFileDirectory)." /p "$(OutDir)Package.msix" /o
</PostBuildEvent>
```
### RC File Icon Path Escaping
Windows Resource Compiler requires double-backslash paths:
```c
// Before (breaks)
IDI_ICON1 ICON "..\\ui\Assets\ImageResizer\ImageResizer.ico"
// After
IDI_ICON1 ICON "..\\ui\\Assets\\ImageResizer\\ImageResizer.ico"
```
### BOM/Encoding Normalization
Migration may strip UTF-8 BOM from C# files (`// Copyright``// Copyright`). This is cosmetic and safe, but be aware it will show as changes in diff.
---
## Test Adaptation
### Tests Requiring WPF Runtime
If tests still need WPF types (e.g., comparing old vs new output), temporarily add:
```xml
<UseWPF>true</UseWPF>
```
Remove this after fully migrating all test code to WinRT APIs.
### Tests Using ResourceLoader
Unit tests cannot easily initialize WinUI 3 `ResourceLoader`. Options:
- Hardcode expected strings in tests: `"Value must be between '{0}' and '{1}'."`
- Delete tests that only verify resource string lookup
- Avoid creating `App` instances in test harness (WinUI App cannot be instantiated in tests)
### Async Test Methods
All imaging tests become async:
```csharp
// Before
[TestMethod]
public void ResizesImage() { ... }
// After
[TestMethod]
public async Task ResizesImageAsync() { ... }
```
### uint Assertions
```csharp
// Before
Assert.AreEqual(96, image.Frames[0].PixelWidth);
// After
Assert.AreEqual(96u, decoder.PixelWidth);
```
### Pixel Data Access in Tests
```csharp
// Before (WPF)
public static Color GetFirstPixel(this BitmapSource source)
{
var pixel = new byte[4];
new FormatConvertedBitmap(
new CroppedBitmap(source, new Int32Rect(0, 0, 1, 1)),
PixelFormats.Bgra32, null, 0).CopyPixels(pixel, 4, 0);
return Color.FromArgb(pixel[3], pixel[2], pixel[1], pixel[0]);
}
// After (WinRT)
public static async Task<(byte R, byte G, byte B, byte A)> GetFirstPixelAsync(
this BitmapDecoder decoder)
{
using var bitmap = await decoder.GetSoftwareBitmapAsync(
BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
var buffer = new Windows.Storage.Streams.Buffer(
(uint)(bitmap.PixelWidth * bitmap.PixelHeight * 4));
bitmap.CopyToBuffer(buffer);
using var reader = DataReader.FromBuffer(buffer);
byte b = reader.ReadByte(), g = reader.ReadByte(),
r = reader.ReadByte(), a = reader.ReadByte();
return (r, g, b, a);
}
```
### Metadata Assertions
```csharp
// Before
Assert.AreEqual("Test", ((BitmapMetadata)image.Frames[0].Metadata).Comment);
// After
var props = await decoder.BitmapProperties.GetPropertiesAsync(
new[] { "System.Photo.DateTaken" });
Assert.IsTrue(props.ContainsKey("System.Photo.DateTaken"),
"Metadata should be preserved during transcode");
```
### AllowUnsafeBlocks for SoftwareBitmap Tests
If tests access pixel data via `IMemoryBufferByteAccess`, add:
```xml
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
```
---
## Settings JSON Backward Compatibility
- Settings are stored in `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\`
- Schema must remain backward-compatible across upgrades
- Add new fields with defaults; never remove or rename existing fields
- Create custom enums matching WPF enum integer values for deserialization (e.g., `ImagingEnums.cs`)
- See: `src/settings-ui/Settings.UI.Library/`
## IPC Contract
If the module communicates with the runner or settings UI:
1. Update BOTH sides of the IPC contract
2. Test settings changes are received by the module
3. Test module state changes are reflected in settings UI
4. Reference: `doc/devdocs/core/settings/runner-ipc.md`
---
## Checklist for PowerToys Module Migration
### Project & Dependencies
- [ ] Update `.csproj`: `UseWPF``UseWinUI`, TFM → `net8.0-windows10.0.19041.0`
- [ ] Add `WindowsPackageType=None`, `SelfContained=true`, `WindowsAppSDKSelfContained=true`
- [ ] Add `DISABLE_XAML_GENERATED_MAIN` if using custom `Program.cs`
- [ ] Replace NuGet packages (WPF-UI → remove, add WindowsAppSDK, etc.)
- [ ] Update project references (GPOWrapperProjection → GPOWrapper + CsWinRT)
- [ ] Move `InternalsVisibleTo` from code to `.csproj`
- [ ] Extract CLI shared logic to Library project (avoid Exe→WinExe dependency)
### MVVM & Resources
- [ ] Replace custom `Observable`/`RelayCommand` with CommunityToolkit.Mvvm source generators
- [ ] Migrate `.resx``.resw` (`Properties/Resources.resx``Strings/en-us/Resources.resw`)
- [ ] Create `ResourceLoaderInstance` helper
- [ ] Wrap resource-dependent statics in `Lazy<T>` or null-coalescing properties
- [ ] Delete `Properties/Resources.Designer.cs`, `Observable.cs`, `RelayCommand.cs`
### XAML
- [ ] Replace `clr-namespace:``using:` in all xmlns declarations
- [ ] Remove WPF-UI (Lepo) xmlns and controls — use native WinUI 3
- [ ] Replace `{x:Static p:Resources.Key}``x:Uid` with `.resw` keys
- [ ] Replace `{DynamicResource}``{ThemeResource}`
- [ ] Replace `DataType="{x:Type ...}"``x:DataType="..."`
- [ ] Replace `<Style.Triggers>``VisualStateManager`
- [ ] Add `<XamlControlsResources/>` to `App.xaml` merged dictionaries
- [ ] Move `Window.Resources` to root container's `Resources`
- [ ] Run XamlStyler: `.\.pipelines\applyXamlStyling.ps1 -Main`
### Code-Behind & APIs
- [ ] Replace all `System.Windows.*` namespaces with `Microsoft.UI.Xaml.*`
- [ ] Replace `Dispatcher` with `DispatcherQueue`
- [ ] Store `DispatcherQueue` reference explicitly (no `Application.Current.Dispatcher`)
- [ ] Implement `SizeToContent()` via AppWindow if needed
- [ ] Update `ContentDialog` calls to set `XamlRoot`
- [ ] Update `FilePicker` calls with HWND initialization
- [ ] Migrate imaging code to `Windows.Graphics.Imaging` (async, `SoftwareBitmap`)
- [ ] Create `CodecHelper` for legacy GUID → WinRT codec ID mapping (if imaging)
- [ ] Create custom imaging enums for JSON backward compatibility (if imaging)
- [ ] Update all `IValueConverter` signatures (`CultureInfo``string`)
### Build & Installer
- [ ] Update WiX installer: remove satellite assembly refs from `Resources.wxs`
- [ ] Run `generateAllFileComponents.ps1`; handle phantom artifacts
- [ ] Update ESRP signing paths to `WinUI3Apps\\`
- [ ] Fix `$(SolutionDir)``$(MSBuildThisFileDirectory)` in build events
- [ ] Move MSIX packaging from PreBuild to PostBuild
- [ ] Fix RC file path escaping (double-backslash)
- [ ] Verify output dir is `WinUI3Apps/`
### Testing & Validation
- [ ] Update test project: async methods, `uint` assertions
- [ ] Handle ResourceLoader unavailability in tests (hardcode strings or skip)
- [ ] Build clean: `cd` to project folder, `tools/build/build.cmd`, exit code 0
- [ ] Run tests for affected module
- [ ] Verify settings JSON backward compatibility
- [ ] Test IPC contracts (runner ↔ settings UI)

View File

@@ -0,0 +1,314 @@
# Threading and Window Management Migration
Based on patterns from the ImageResizer migration.
## Dispatcher → DispatcherQueue
### API Mapping
| WPF | WinUI 3 |
|-----|---------|
| `Dispatcher.Invoke(Action)` | `DispatcherQueue.TryEnqueue(Action)` |
| `Dispatcher.BeginInvoke(Action)` | `DispatcherQueue.TryEnqueue(Action)` |
| `Dispatcher.Invoke(DispatcherPriority, Action)` | `DispatcherQueue.TryEnqueue(DispatcherQueuePriority, Action)` |
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` |
| `Dispatcher.VerifyAccess()` | Check `DispatcherQueue.HasThreadAccess` (no exception-throwing method) |
### Priority Mapping
WinUI 3 has only 3 levels: `High`, `Normal`, `Low`.
| WPF `DispatcherPriority` | WinUI 3 `DispatcherQueuePriority` |
|-------------------------|----------------------------------|
| `Send` | `High` |
| `Normal` / `Input` / `Loaded` / `Render` / `DataBind` | `Normal` |
| `Background` / `ContextIdle` / `ApplicationIdle` / `SystemIdle` | `Low` |
### Pattern: Global DispatcherQueue Access (from ImageResizer)
WPF provided `Application.Current.Dispatcher` globally. WinUI 3 requires explicit storage:
```csharp
// Store DispatcherQueue at app startup
private static DispatcherQueue _uiDispatcherQueue;
public static void InitializeDispatcher()
{
_uiDispatcherQueue = DispatcherQueue.GetForCurrentThread();
}
```
Usage with thread-check pattern (from `Settings.Reload()`):
```csharp
var currentDispatcher = DispatcherQueue.GetForCurrentThread();
if (currentDispatcher != null)
{
// Already on UI thread
ReloadCore(jsonSettings);
}
else if (_uiDispatcherQueue != null)
{
// Dispatch to UI thread
_uiDispatcherQueue.TryEnqueue(() => ReloadCore(jsonSettings));
}
else
{
// Fallback (e.g., CLI mode, no UI)
ReloadCore(jsonSettings);
}
```
### Pattern: DispatcherQueue in ViewModels (from ProgressViewModel)
```csharp
public class ProgressViewModel
{
private readonly DispatcherQueue _dispatcherQueue;
public ProgressViewModel()
{
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
}
private void OnProgressChanged(double progress)
{
_dispatcherQueue.TryEnqueue(() =>
{
Progress = progress;
// other UI updates...
});
}
}
```
### Pattern: Async Dispatch (await)
```csharp
// WPF
await this.Dispatcher.InvokeAsync(() => { /* UI work */ });
// WinUI 3 (using TaskCompletionSource)
var tcs = new TaskCompletionSource();
this.DispatcherQueue.TryEnqueue(() =>
{
try { /* UI work */ tcs.SetResult(); }
catch (Exception ex) { tcs.SetException(ex); }
});
await tcs.Task;
```
### C++/WinRT Threading
| Old API | New API |
|---------|---------|
| `winrt::resume_foreground(CoreDispatcher)` | `wil::resume_foreground(DispatcherQueue)` |
| `CoreDispatcher.RunAsync()` | `DispatcherQueue.TryEnqueue()` |
Add `Microsoft.Windows.ImplementationLibrary` NuGet for `wil::resume_foreground`.
---
## Window Management
### WPF Window vs WinUI 3 Window
| Feature | WPF `Window` | WinUI 3 `Window` |
|---------|-------------|------------------|
| Base class | `ContentControl``DependencyObject` | **NOT** a control, NOT a `DependencyObject` |
| `Resources` property | Yes | No — use root container's `Resources` |
| `DataContext` property | Yes | No — use root `Page`/`UserControl` |
| `VisualStateManager` | Yes | No — use inside child controls |
| `Load`/`Unload` events | Yes | No |
| `SizeToContent` | Yes (`Height`/`Width`/`WidthAndHeight`) | No — must implement manually |
| `WindowState` (min/max/normal) | Yes | No — use `AppWindow.Presenter` |
| `WindowStyle` | Yes | No — use `AppWindow` title bar APIs |
| `ResizeMode` | Yes | No — use `AppWindow.Presenter` |
| `WindowStartupLocation` | Yes | No — calculate manually |
| `Icon` | `Window.Icon` | `AppWindow.SetIcon()` |
| `Title` | `Window.Title` | `AppWindow.Title` (or `Window.Title`) |
| Size (Width/Height) | Yes | No — use `AppWindow.Resize()` |
| Position (Left/Top) | Yes | No — use `AppWindow.Move()` |
| `IsDefault`/`IsCancel` on buttons | Yes | No — handle Enter/Escape in code-behind |
### Getting AppWindow from Window
```csharp
using Microsoft.UI;
using Microsoft.UI.Windowing;
using WinRT.Interop;
IntPtr hwnd = WindowNative.GetWindowHandle(window);
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
```
### Pattern: SizeToContent Replacement (from ImageResizer)
WinUI 3 has no `SizeToContent`. ImageResizer implemented a manual equivalent:
```csharp
private void SizeToContent()
{
if (Content is not FrameworkElement content)
return;
// Measure desired content size
content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
var desiredHeight = content.DesiredSize.Height + WindowChromeHeight + Padding;
// Account for DPI scaling
var scaleFactor = Content.XamlRoot.RasterizationScale;
var pixelHeight = (int)(desiredHeight * scaleFactor);
var pixelWidth = (int)(WindowWidth * scaleFactor);
// Resize via AppWindow
var hwnd = WindowNative.GetWindowHandle(this);
var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
var appWindow = AppWindow.GetFromWindowId(windowId);
appWindow.Resize(new Windows.Graphics.SizeInt32(pixelWidth, pixelHeight));
}
```
**Key details:**
- `WindowChromeHeight` ≈ 32px for the title bar
- Must multiply by `RasterizationScale` for DPI-aware sizing
- Call `SizeToContent()` after page navigation or content changes
- Unsubscribe previous event handlers before subscribing new ones to avoid memory leaks
### Window Positioning (Center Screen)
```csharp
var displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest);
var centerX = (displayArea.WorkArea.Width - appWindow.Size.Width) / 2;
var centerY = (displayArea.WorkArea.Height - appWindow.Size.Height) / 2;
appWindow.Move(new Windows.Graphics.PointInt32(centerX, centerY));
```
### Window State (Minimize/Maximize)
```csharp
(appWindow.Presenter as OverlappedPresenter)?.Maximize();
(appWindow.Presenter as OverlappedPresenter)?.Minimize();
(appWindow.Presenter as OverlappedPresenter)?.Restore();
```
### Title Bar Customization
```csharp
// Extend content into title bar
this.ExtendsContentIntoTitleBar = true;
this.SetTitleBar(AppTitleBar); // AppTitleBar is a XAML element
// Or via AppWindow API
if (AppWindowTitleBar.IsCustomizationSupported())
{
var titleBar = appWindow.TitleBar;
titleBar.ExtendsContentIntoTitleBar = true;
titleBar.ButtonBackgroundColor = Colors.Transparent;
}
```
### Tracking the Main Window
```csharp
public partial class App : Application
{
public static Window MainWindow { get; private set; }
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
MainWindow = new MainWindow();
MainWindow.Activate();
}
}
```
### ContentDialog Requires XamlRoot
```csharp
var dialog = new ContentDialog
{
Title = "Confirm",
Content = "Are you sure?",
PrimaryButtonText = "Yes",
CloseButtonText = "No",
XamlRoot = this.Content.XamlRoot // REQUIRED
};
var result = await dialog.ShowAsync();
```
### File Pickers Require HWND
```csharp
var picker = new FileOpenPicker();
picker.FileTypeFilter.Add(".jpg");
// REQUIRED for desktop apps
var hwnd = WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
var file = await picker.PickSingleFileAsync();
```
### Window Close Handling
```csharp
// WPF
protected override void OnClosing(CancelEventArgs e) { e.Cancel = true; this.Hide(); }
// WinUI 3
this.AppWindow.Closing += (s, e) => { e.Cancel = true; this.AppWindow.Hide(); };
```
---
## Custom Entry Point (DISABLE_XAML_GENERATED_MAIN)
ImageResizer uses a custom `Program.cs` entry point instead of the WinUI 3 auto-generated `Main`. This is needed for:
- CLI mode (process files without showing UI)
- Custom initialization before the WinUI 3 App starts
- Single-instance enforcement
### Setup
In `.csproj`:
```xml
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
```
Create `Program.cs`:
```csharp
public static class Program
{
[STAThread]
public static int Main(string[] args)
{
if (args.Length > 0)
{
// CLI mode — no UI
return RunCli(args);
}
// GUI mode
WinRT.ComWrappersSupport.InitializeComWrappers();
Application.Start((p) =>
{
var context = new DispatcherQueueSynchronizationContext(
DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
_ = new App();
});
return 0;
}
}
```
### WPF App Constructor Removal
WPF modules often created `new App()` to initialize the WPF `Application` and get `Application.Current.Dispatcher`. This is no longer needed — the WinUI 3 `Application.Start()` handles this.
```csharp
// DELETE (WPF pattern):
_imageResizerApp = new App();
// REPLACE with: Store DispatcherQueue explicitly (see Global DispatcherQueue Access above)
```

View File

@@ -0,0 +1,365 @@
# XAML Migration Guide
Detailed reference for migrating XAML from WPF to WinUI 3, based on the ImageResizer migration.
## XML Namespace Declaration Changes
### Before (WPF)
```xml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyApp"
xmlns:m="clr-namespace:ImageResizer.Models"
xmlns:p="clr-namespace:ImageResizer.Properties"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
x:Class="MyApp.MainWindow">
```
### After (WinUI 3)
```xml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyApp"
xmlns:m="using:ImageResizer.Models"
xmlns:converters="using:ImageResizer.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Class="MyApp.MainWindow">
```
### Key Changes
| WPF Syntax | WinUI 3 Syntax | Notes |
|------------|---------------|-------|
| `clr-namespace:Foo` | `using:Foo` | CLR namespace mapping |
| `clr-namespace:Foo;assembly=Bar` | `using:Foo` | Assembly qualification not needed |
| `xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"` | **Remove entirely** | WPF-UI namespace no longer needed |
| `xmlns:p="clr-namespace:...Properties"` | **Remove** | No more `.resx` string bindings |
| `sys:String` (from mscorlib) | `x:String` | XAML intrinsic types |
| `sys:Int32` | `x:Int32` | XAML intrinsic types |
| `sys:Boolean` | `x:Boolean` | XAML intrinsic types |
| `sys:Double` | `x:Double` | XAML intrinsic types |
## Unsupported Markup Extensions
| WPF Markup Extension | WinUI 3 Alternative |
|----------------------|---------------------|
| `{DynamicResource Key}` | `{ThemeResource Key}` (theme-reactive) or `{StaticResource Key}` |
| `{x:Static Type.Member}` | `{x:Bind}` to a static property, or code-behind |
| `{x:Type local:MyType}` | Not supported; use code-behind |
| `{x:Array}` | Not supported; create collections in code-behind |
| `{x:Code}` | Not supported |
### DynamicResource → ThemeResource
```xml
<!-- WPF -->
<TextBlock Foreground="{DynamicResource MyBrush}" />
<!-- WinUI 3 -->
<TextBlock Foreground="{ThemeResource MyBrush}" />
```
`ThemeResource` automatically updates when the app theme changes (Light/Dark/HighContrast). For truly dynamic non-theme resources, set values in code-behind or use data binding.
### x:Static Resource Strings → x:Uid
This is the most pervasive XAML change. WPF used `{x:Static}` to bind to strongly-typed `.resx` resource strings. WinUI 3 uses `x:Uid` with `.resw` files.
**WPF:**
```xml
<Button Content="{x:Static p:Resources.Cancel}" />
<TextBlock Text="{x:Static p:Resources.Input_Header}" />
```
**WinUI 3:**
```xml
<Button x:Uid="Cancel" />
<TextBlock x:Uid="Input_Header" />
```
In `Strings/en-us/Resources.resw`:
```xml
<data name="Cancel.Content" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="Input_Header.Text" xml:space="preserve">
<value>Select a size</value>
</data>
```
The `x:Uid` suffix (`.Content`, `.Text`, `.Header`, `.PlaceholderText`, etc.) matches the target property name.
### DataType with x:Type → Remove
**WPF:**
```xml
<DataTemplate DataType="{x:Type m:ResizeSize}">
```
**WinUI 3:**
```xml
<DataTemplate x:DataType="m:ResizeSize">
```
## WPF-UI (Lepo) Controls Removal
If the module uses the `WPF-UI` library, replace all Lepo controls with native WinUI 3 equivalents.
### Window
```xml
<!-- WPF (WPF-UI) -->
<ui:FluentWindow
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<ui:TitleBar Title="Image Resizer" />
...
</ui:FluentWindow>
<!-- WinUI 3 (native) -->
<Window>
<!-- Title bar managed via code-behind: this.ExtendsContentIntoTitleBar = true; -->
...
</Window>
```
### App.xaml Resources
```xml
<!-- WPF (WPF-UI) -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark" />
<ui:ControlsDictionary />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<!-- WinUI 3 (native) -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
```
### Common Control Replacements
```xml
<!-- WPF-UI NumberBox -->
<ui:NumberBox Value="{Binding Width}" />
<!-- WinUI 3 -->
<NumberBox Value="{x:Bind ViewModel.Width, Mode=TwoWay}" />
<!-- WPF-UI InfoBar -->
<ui:InfoBar Title="Warning" Message="..." IsOpen="True" Severity="Warning" />
<!-- WinUI 3 -->
<InfoBar Title="Warning" Message="..." IsOpen="True" Severity="Warning" />
<!-- WPF-UI ProgressRing -->
<ui:ProgressRing IsIndeterminate="True" />
<!-- WinUI 3 -->
<ProgressRing IsActive="True" />
<!-- WPF-UI SymbolIcon -->
<ui:SymbolIcon Symbol="Add" />
<!-- WinUI 3 -->
<SymbolIcon Symbol="Add" />
```
### Button Patterns
```xml
<!-- WPF -->
<Button IsDefault="True" Content="OK" />
<Button IsCancel="True" Content="Cancel" />
<!-- WinUI 3 (no IsDefault/IsCancel) -->
<Button Style="{StaticResource AccentButtonStyle}" Content="OK" />
<Button Content="Cancel" />
<!-- Handle Enter/Escape keys in code-behind if needed -->
```
## Style and Template Changes
### Triggers → VisualStateManager
WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported.
**WPF:**
```xml
<Style TargetType="Button">
<Style.Triggers>
<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>
```
**WinUI 3:**
```xml
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid x:Name="RootGrid" Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="LightBlue"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
```
### No Binding in Setter.Value
```xml
<!-- WPF (works) -->
<Setter Property="Foreground" Value="{Binding TextColor}"/>
<!-- WinUI 3 (does NOT work — use StaticResource) -->
<Setter Property="Foreground" Value="{StaticResource TextColorBrush}"/>
```
### Visual State Name Changes
| WPF | WinUI 3 |
|-----|---------|
| `MouseOver` | `PointerOver` |
| `Disabled` | `Disabled` |
| `Pressed` | `Pressed` |
## Resource Dictionary Changes
### Window.Resources → Grid.Resources
WinUI 3 `Window` is NOT a `DependencyObject` — no `Window.Resources`, `DataContext`, or `VisualStateManager`.
```xml
<!-- WPF -->
<Window>
<Window.Resources>
<SolidColorBrush x:Key="MyBrush" Color="Red"/>
</Window.Resources>
<Grid>...</Grid>
</Window>
<!-- WinUI 3 -->
<Window>
<Grid>
<Grid.Resources>
<SolidColorBrush x:Key="MyBrush" Color="Red"/>
</Grid.Resources>
...
</Grid>
</Window>
```
### Theme Dictionaries
```xml
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="MyBrush" Color="#FF000000"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="MyBrush" Color="#FFFFFFFF"/>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="MyBrush" Color="{ThemeResource SystemColorWindowTextColor}"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
```
## URI Scheme Changes
| WPF | WinUI 3 |
|-----|---------|
| `pack://application:,,,/MyAssembly;component/image.png` | `ms-appx:///Assets/image.png` |
| `pack://application:,,,/image.png` | `ms-appx:///image.png` |
| Relative path `../image.png` | `ms-appx:///image.png` |
Assets directory convention: `Resources/``Assets/<Module>/`
## Data Binding Changes
### {Binding} vs {x:Bind}
Both are available. Prefer `{x:Bind}` for compile-time safety and performance.
| Feature | `{Binding}` | `{x:Bind}` |
|---------|------------|------------|
| Default mode | `OneWay` | **`OneTime`** (explicit `Mode=OneWay` required!) |
| Context | `DataContext` | Code-behind class |
| Resolution | Runtime | Compile-time |
| Performance | Reflection-based | Compiled |
| Function binding | No | Yes |
### WPF-Specific Binding Features to Remove
```xml
<!-- These WPF-only features must be removed or rewritten -->
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
<!-- WinUI 3: UpdateSourceTrigger not needed; TextBox uses PropertyChanged by default -->
<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 -->
<ItemsControl ItemsSource="{Binding}" />
<!-- WinUI 3: Must specify explicit path -->
<ItemsControl ItemsSource="{x:Bind ViewModel.Items}" />
```
## WPF-Only Window Properties to Remove
These properties exist on WPF `Window` but not WinUI 3:
```xml
<!-- Remove from XAML — handle in code-behind via AppWindow API -->
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
ExtendsContentIntoTitleBar="True" <!-- Set in code-behind -->
```
## XAML Control Property Changes
| WPF Property | WinUI 3 Property | Notes |
|-------------|-----------------|-------|
| `Focusable` | `IsTabStop` | Different name |
| `SnapsToDevicePixels` | Not available | WinUI handles pixel snapping internally |
| `UseLayoutRounding` | `UseLayoutRounding` | Same |
| `IsHitTestVisible` | `IsHitTestVisible` | Same |
| `TextBox.VerticalScrollBarVisibility` | `ScrollViewer.VerticalScrollBarVisibility` (attached) | Attached property |
## XAML Formatting (XamlStyler)
After migration, run XamlStyler to normalize formatting:
- Alphabetize xmlns declarations and element attributes
- Add UTF-8 BOM to all XAML files
- Normalize comment spacing: `<!-- text -->``<!-- text -->`
PowerToys command: `.\.pipelines\applyXamlStyling.ps1 -Main`

5
.gitignore vendored
View File

@@ -360,3 +360,8 @@ src/common/Telemetry/*.etl
# PowerToysInstaller Build Temp Files
installer/*/*.wxs.bk
/src/modules/awake/.claude
# Squad / Copilot agents — local-only, not committed
.squad/
.squad-workstream
.github/agents/

View File

@@ -106,7 +106,13 @@
"PowerToys.SvgThumbnailProvider.dll",
"PowerToys.SvgThumbnailProvider.exe",
"PowerToys.SvgThumbnailProviderCpp.dll",
"PowerToys.KeyboardManager.dll",
"KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
"WinUI3Apps\\PowerToys.KeyboardManagerEditorUI.exe",
"WinUI3Apps\\PowerToys.KeyboardManagerEditorUI.dll",
"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
"PowerToys.KeyboardManagerEditorLibraryWrapper.dll",
"WinUI3Apps\\PowerToys.HostsModuleInterface.dll",
"WinUI3Apps\\PowerToys.HostsUILib.dll",
"WinUI3Apps\\PowerToys.Hosts.dll",

View File

@@ -9,7 +9,7 @@ schedules:
always: false # only run if there's code changes!
pool:
vmImage: windows-2019
vmImage: windows-latest
resources:
repositories:

View File

@@ -210,6 +210,9 @@ jobs:
& '.pipelines/applyXamlStyling.ps1' -Passive
displayName: Verify XAML formatting
- task: NuGetAuthenticate@1
displayName: Authenticate NuGet feeds for verification
- pwsh: |-
& '.pipelines/verifyNugetPackages.ps1' -solution '$(build.sourcesdirectory)\PowerToys.slnx'
displayName: Verify Nuget package versions for PowerToys.slnx

View File

@@ -17,10 +17,10 @@ $nonDirectoryAssetsItems = Get-ChildItem $targetAssetsDir -Attributes !Directory
$directoryAssetsItems = Get-ChildItem $targetAssetsDir -Attributes Directory
if ($directoryAssetsItems.Count -le 0) {
Write-Host -ForegroundColor Red "No directories detected in " $nonDirectoryAssetsItems ". Are you sure this is the right path?`r`n"
Write-Host -ForegroundColor Red "ERROR: No directories detected in " $nonDirectoryAssetsItems ". Are you sure this is the right path?`r`n"
$totalFailures++;
} elseif ($nonDirectoryAssetsItems.Count -gt 0) {
Write-Host -ForegroundColor Red "Detected " $nonDirectoryAssetsItems " files in " $targetAssetsDir "`r`n"
Write-Host -ForegroundColor Red "ERROR: Detected " $nonDirectoryAssetsItems " files in " $targetAssetsDir ". Each application should use a named subdirectory for assets.`r`n"
$totalFailures++;
} else {
Write-Host -ForegroundColor Green "Only directories detected in " $targetAssetsDir "`r`n"
@@ -29,7 +29,7 @@ if ($directoryAssetsItems.Count -le 0) {
# Make sure there's no resources.pri file. Each application should use a different name for their own resources file path.
$resourcesPriFiles = Get-ChildItem $targetDir -Filter resources.pri
if ($resourcesPriFiles.Count -gt 0) {
Write-Host -ForegroundColor Red "Detected a resources.pri file in " $targetDir "`r`n"
Write-Host -ForegroundColor Red "ERROR: Detected a resources.pri file in " $targetDir ". Each application should use a unique name for its resources file.`r`n"
$totalFailures++;
} else {
Write-Host -ForegroundColor Green "No resources.pri file detected in " $targetDir "`r`n"
@@ -38,7 +38,7 @@ if ($resourcesPriFiles.Count -gt 0) {
# Each application should have their XAML files in their own paths to avoid these conflicts.
$resourcesPriFiles = Get-ChildItem $targetDir -Filter *.xbf
if ($resourcesPriFiles.Count -gt 0) {
Write-Host -ForegroundColor Red "Detected a .xbf file in " $targetDir "`r`n"
Write-Host -ForegroundColor Red "ERROR: Detected a .xbf file in " $targetDir ". Ensure all XAML files are placed in a subdirectory in each application.`r`n"
$totalFailures++;
} else {
Write-Host -ForegroundColor Green "No .xbf files detected in " $targetDir "`r`n"

View File

@@ -20,6 +20,7 @@
<NuGetAuditMode>direct</NuGetAuditMode>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <!-- Don't add source revision hash to the product version of binaries. -->
<PlatformTarget>$(Platform)</PlatformTarget>
<RestoreEnablePackagePruning Condition=" '$(VisualStudioVersion)' == '17.0'">false </RestoreEnablePackagePruning>
<!-- Enable Microsoft.Testing.Platform -->
<EnableMSTestRunner>true</EnableMSTestRunner>

View File

@@ -40,7 +40,6 @@
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
<PackageVersion Include="MessagePack" Version="3.1.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.5.250829002" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />

View File

@@ -497,6 +497,31 @@
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngine/KeyboardManagerEngine.vcxproj" Id="ba661f5b-1d5a-4ffc-9bf1-fc39df280bdd" />
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManagerEngineLibrary.vcxproj" Id="e496b7fc-1e99-4bab-849b-0e8367040b02" />
</Folder>
<Folder Name="/modules/MouseUtils/">
<Project Path="src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj" Id="48a1db8c-5df8-4fb3-9e14-2b67f3f2d8b5" />
<Project Path="src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj" Id="e94fd11c-0591-456f-899f-efc0ca548336" />
<Project Path="src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj" Id="782a61be-9d85-4081-b35c-1ccc9dcc1e88" />
<Project Path="src/modules/MouseUtils/MouseJump.Common/MouseJump.Common.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseJump/MouseJump.vcxproj" Id="8a08d663-4995-40e3-b42c-3f910625f284" />
<Project Path="src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj" Id="eae14c0e-7a6b-45da-9080-a7d8c077ba6e" />
</Folder>
<Folder Name="/modules/MouseUtils/Tests/">
<Project Path="src/modules/MouseUtils/MouseJump.Common.UnitTests/MouseJump.Common.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseUtils.UITests/MouseUtils.UITests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/keyboardmanager/Tests/">
<Project Path="src/modules/keyboardmanager/KeyboardManagerEditorTest/KeyboardManagerEditorTest.vcxproj" Id="62173d9a-6724-4c00-a1c8-fb646480a9ec" />
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngineTest/KeyboardManagerEngineTest.vcxproj" Id="7f4b3a60-bc27-45a7-8000-68b0b6ea7466" />
@@ -720,31 +745,6 @@
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/MouseUtils/">
<Project Path="src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj" Id="48a1db8c-5df8-4fb3-9e14-2b67f3f2d8b5" />
<Project Path="src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj" Id="e94fd11c-0591-456f-899f-efc0ca548336" />
<Project Path="src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj" Id="782a61be-9d85-4081-b35c-1ccc9dcc1e88" />
<Project Path="src/modules/MouseUtils/MouseJump.Common/MouseJump.Common.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseJump/MouseJump.vcxproj" Id="8a08d663-4995-40e3-b42c-3f910625f284" />
<Project Path="src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj" Id="eae14c0e-7a6b-45da-9080-a7d8c077ba6e" />
</Folder>
<Folder Name="/modules/MouseUtils/Tests/">
<Project Path="src/modules/MouseUtils/MouseJump.Common.UnitTests/MouseJump.Common.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseUtils.UITests/MouseUtils.UITests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/MouseWithoutBorders/">
<Project Path="src/modules/MouseWithoutBorders/App/Helper/MouseWithoutBordersHelper.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />

256
README.md
View File

@@ -51,19 +51,19 @@ But to get started quickly, choose one of the installation methods below:
Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a>, click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.98%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysUserSetup-0.97.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysUserSetup-0.97.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysSetup-0.97.1-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysSetup-0.97.1-arm64.exe
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.99%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.98%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysUserSetup-0.98.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysUserSetup-0.98.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysSetup-0.98.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.0/PowerToysSetup-0.98.0-arm64.exe
| Description | Filename |
|----------------|----------|
| Per user - x64 | [PowerToysUserSetup-0.97.1-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.97.1-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.97.1-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.97.1-arm64.exe][ptMachineArm64] |
| Per user - x64 | [PowerToysUserSetup-0.98.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.98.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.98.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.98.0-arm64.exe][ptMachineArm64] |
</details>
@@ -102,242 +102,14 @@ winget install --scope machine Microsoft.PowerToys -s winget
There are <a href="https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools">community driven install methods</a> such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
</details>
## ✨ What's new
## ✨ What's new?
**Version 0.97.2 (Feb 2026)**
[![What's new image](doc/images/readme/Release-Banner.png)](https://github.com/microsoft/PowerToys/releases)
This patch release fixes several important stability issues identified in v0.97.0 based on incoming reports. Check out the [v0.97.0](https://github.com/microsoft/PowerToys/releases/tag/v0.97.0) notes for the full list of changes.
## Advanced Paste
- #45207 Fixed a crash in the Advanced Paste settings page caused by null values during JSON deserialization.
## Color Picker
- #45367 Fixed contrast issue in Color picker UI.
## Command Palette
- #45194 Fixed an issue where some Command Palette PowerToys Extension strings were not localised.
## Cursor Wrap
- #45210 Fixed "Automatically activate on utility startup" setting not persisting when disabled. Thanks [@ThanhNguyxn](https://github.com/ThanhNguyxn)!
- #45303 Added option to disable Cursor Wrapping when only a single monitor is connected. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
## Image Resizer
- #45184 Fixed Image Resizer not working after upgrading PowerToys on Windows 10 by properly cleaning up legacy sparse app packages.
## LightSwitch
- #45304 Fixed Light Switch startup logic to correctly apply the appropriate theme on launch.
## Workspaces
- #45183 Fixed overlay positioning issue in workspace snapshot draw caused by DPI-aware coordinate mismatch.
## Quick Access and Measure Tool
- #45443 Fixed crash related to `IsShownInSwitchers` property when Explorer is not running.
**Version 0.97.1 (January 2026)**
**Highlights**
### Advanced Paste
- #44862: Fixed Settings UI advanced paste page crash by using correct settings repository for null checking.
### Command Palette
- #44886: Fixed personalization section not appearing by using latest MSIX for installation.
- #44938: Fixed loading of icons from internet shortcuts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- #45076: Fixed potential deadlock from lazy-loading AppListItem details. Thanks [@jiripolasek](https://github.com/jiripolasek)!
### Cursor Wrap
- #44936: Added improved multi-monitor support; Added laptop lid close detection for dynamic monitor topology updates. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
- #44936: Added new settings dropdown to constrain wrapping to horizontal-only, vertical-only, or both directions. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
### Peek
- #44995: Fixed Space key triggering Peek during file rename, search, or address bar typing.
### PowerRename
- #44944: Fixed regex `$` not working, preventing users from adding text at the end of filenames.
### Runner
- #44931: Monochrome tray icon now adapts to Windows system theme instead of app theme.
- #44982: Fixed right-click menu to dynamically update based on Quick Access enabled/disabled state.
### GPO / Enterprise
- #45028: Added CursorWrap policy definition to ADMX templates. Thanks [@htcfreek](https://github.com/htcfreek)!
For the full list of v0.97 changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
## Advanced Paste
- Added hex color previews in clipboard history. Thanks [@crramirez](https://github.com/crramirez)!
- Added automatic placeholder endpoints when required fields are left empty.
- Fixed a grammar issue in the AI settings description. Thanks [@erik-anderson](https://github.com/erik-anderson)!
- Fixed loading order so custom action hotkeys are read correctly.
- Updated Advanced Paste descriptions to reflect support for online and local models.
- Fixed clipboard history item selection so it doesnt duplicate entries.
- Prevented placeholder endpoints from being saved for providers that dont need them.
- Added image input support for AI transforms and improved clipboard change tracking.
## Awake
- Fixed Awake CLI so help, errors, and logs appear correctly in the console. Thanks [@daverayment](https://github.com/daverayment)!
## Command Palette
- Fixed background image loading in BlurImageControl. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed SDK packaging paths and added a CI SDK build stage.
- Aligned naming and spell-checking with .NET conventions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added drag-and-drop support for Command Palette items. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added a PowerToys Command Palette extension to discover and launch PowerToys utilities.
- Fixed grid view bindings and layout issues. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Fixed a line-break issue in RDC extension toast messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made the Settings button text localizable. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Hid the RDC fallback on the home page and fixed MSTSC working directory handling. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Optimized result list merging for better performance. Thanks [@daverayment](https://github.com/daverayment)!
- Added Small/Medium/Large detail sizes in the extensions API. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
- Hid fallback commands on the home page when no query is entered. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added back navigation support in the Settings window. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added a Command Palette solution filter. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Updated Extension SDK documentation links to Microsoft Learn. Thanks [@RubenFricke](https://github.com/RubenFricke)!
- Added a custom search engine URL setting for Web Search. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added pinyin matching for Chinese input. Thanks [@frg2089](https://github.com/frg2089)!
- Bumped Command Palette version to 0.8.
- Removed subtitles from built-in top-level commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Refined separator styling in the details pane. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added a built-in Remote Desktop extension.
- Added a Peek command to the Indexer extension.
- Improved default browser detection using the Windows Shell API. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added Escape key behavior options. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added theme and background customization options. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved WinGet package app matching. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added an auto-return-home delay setting. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added fallback ranking and global results settings.
- Removed the selection indicator in the context menu list. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added a developer ribbon with build and log info. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Updated the “Learn more” string for Command Palette. Thanks [@pratnala](https://github.com/pratnala)!
- Added arrow-key navigation for grid views. Thanks [@samrueby](https://github.com/samrueby)!
- Fixed version display when running unpackaged. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added a native debugging launch profile. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Reduced redundant property change notifications in the SDK. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Improved section readability and accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made gallery spacing uniform. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Added sections and separators for list and grid pages. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
## Crop & Lock
- Added a screenshot mode that freezes a cropped region into its own window. Thanks [@fm-sys](https://github.com/fm-sys)!
## Cursor Wrap
- Improved Cursor Wrap behavior on multi-monitor setups by wrapping only at outer edges. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
## FancyZones
- Fixed editor overlay positioning on mixed-DPI multi-monitor setups. Thanks [@Memphizzz](https://github.com/Memphizzz)!
- Added a FancyZones CLI for command-line layout management.
## File Locksmith
- Added a File Locksmith CLI for querying, waiting on, or killing file locks.
## Find My Mouse
- Improved spotlight edge rendering for clearer Find My Mouse visuals.
- Added telemetry to track how Find My Mouse is triggered.
## Image Resizer
- Fixed Fill mode cropping when Shrink Only is enabled. Thanks [@daverayment](https://github.com/daverayment)!
- Added a dedicated Image Resizer CLI for scripted resizing.
## Light Switch
- Added telemetry events for Light Switch usage and settings changes.
- Added a Follow Night Light mode to sync theme changes with Night Light.
- Clarified LightSwitchService and LightSwitchStateManager roles in docs.
- Added a Quick Access dashboard button to toggle Light Switch quickly.
- Ensured Light Switch honors GPO policy states with clear status messaging.
## Mouse Without Borders
- Continued refactoring Mouse Without Borders by splitting the large Common class into focused components. Thanks [@mikeclayton](https://github.com/mikeclayton)!
- Completed the Common class refactor with Core and IPC helper extraction. Thanks [@mikeclayton](https://github.com/mikeclayton)!
## Peek
- Hardened Peek previews with strict resource filtering and safer external link warnings.
- Improved SVG preview compatibility by rendering via WebView2.
## PowerRename
- Added HEIF/AVIF EXIF metadata extraction and extension status guidance for related previews.
- Fixed undefined behavior in file time handling. Thanks [@safocl](https://github.com/safocl)!
- Optimized memory allocation for depth-based rename processing.
- Fixed Unicode normalization and nonbreaking space matching. Thanks [@daverayment](https://github.com/daverayment)!
- Fixed date token replacements followed by capital letters. Thanks [@daverayment](https://github.com/daverayment)!
## PowerToys Run Plugins
- Fixed a plugin name typo and added Project Launcher to the thirdparty list. Thanks [@artickc](https://github.com/artickc)!
- Added the Open With Antigravity plugin to the thirdparty list. Thanks [@artickc](https://github.com/artickc)!
## PowerToys Run
- Avoided unnecessary hotkey conflict checks when settings change.
- Added QuickAI to the third-party PowerToys Run plugin list. Thanks [@ruslanlap](https://github.com/ruslanlap)!
## Quick Accent
- Added localized quotation marks to Quick Accent. Thanks [@warquys](https://github.com/warquys)!
- Fixed duplicate and redundant characters in Quick Accent sets. Thanks [@noraa-junker](https://github.com/noraa-junker)!
- Fixed DPI positioning issues for Quick Accent on mixed-DPI setups. Thanks [@noraa-junker](https://github.com/noraa-junker)!
## Settings
- Added a new tray icon that adapts to theme changes. Thanks [@HO-COOH](https://github.com/HO-COOH)!
- Centralized module enable/disable logic for cleaner Settings UI updates.
- Simplified Settings utilities by removing ISettingsUtils/ISettingsPath interfaces. Thanks [@noraa-junker](https://github.com/noraa-junker)!
- Improved Settings UI consistency and disabled-state visuals.
- Added semantic headings to the Dashboard for better accessibility.
- Introduced Quick Access as a standalone host with updated Settings integration.
- Fixed Dashboard toggle flicker and sort menu checkmarks. Thanks [@daverayment](https://github.com/daverayment)!
- Added Native AOT-compatible settings serialization.
- Standardized mouse tool description text. Thanks [@daverayment](https://github.com/daverayment)!
- Added a global SettingsUtils singleton to reduce repeated initialization.
## Development
- Fixed broken devdocs links to the coding style guide. Thanks [@RubenFricke](https://github.com/RubenFricke)!
- Migrated main and installer solutions to .slnx for improved build tooling.
- Restored local installer builds after the WiX v5 upgrade with signing and versioning fixes.
- Added incremental review tooling and structured AI prompts for PR/issue reviews.
- Documented bot commands and cleaned up devdocs structure. Thanks [@noraa-junker](https://github.com/noraa-junker)!
- Updated WinAppSDK pipeline defaults to 1.8 and fixed restore handling.
- Updated the COMMUNITY list to reflect current roles.
- Maintained community member ordering and added a new entry.
- Re-enabled centralized PackageReference for native projects with VS auto-restore.
- Disabled MSBuild caching by default in CI to avoid build instability.
- Updated the latest WinAppSDK daily pipeline for split-dependency restores.
- Suppressed experimental build warnings and aligned WrapPanel stretch handling.
- Reordered the spell-check expect list for consistent automation.
- Migrated native projects to centralized PackageReference management.
- Cleaned spell-check dictionary entries and capitalization.
- Synced commit/PR prompts and wired VS Code to repo prompt files.
- Added VS Code build tasks and improved build script path handling.
- Updated Windows App SDK package versions in central package management.
- Migrated cmdpal extension native project to PackageReference and fixed outputs.
- Reverted PackageReference changes back to packages.config where needed.
- Bypassed a release version check for a failing DLL to keep pipelines green.
- Consolidated Copilot instructions and fixed prompt frontmatter.
- Added signing entries for new Quick Access binaries and CLI version metadata.
- Fixed install scope detection to avoid mixed per-user/per-machine installs.
- Added a Module Loader tool to quickly test PowerToys modules without full builds. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
- Added update telemetry to understand auto-update checks and downloads.
- Updated the telemetry package for new compliance requirements. Thanks [@carlos-zamora](https://github.com/carlos-zamora)!
- Documented missing telemetry events in DATA_AND_PRIVACY.
- Fixed UI test pipeline restores for .slnx solutions.
- Added UI automation coverage for Advanced Paste clipboard history flows.
- Stabilized FancyZones UI tests with more reliable selectors and screen recordings.
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/tag/v0.98.0).
## 🛣️ Roadmap
We are planning some nice new features and improvements for the next releases PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.98][github-next-release-work]!
We are planning some nice new features and improvements for the next releases PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.99][github-next-release-work]!
## ❤️ PowerToys Community
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!

View File

@@ -141,3 +141,10 @@ Note: The DllHost process loads the DLL only when the context menu is triggered
- A signature issue with the MSIX package
- For development and testing, using the Windows 10 handler can be easier since it doesn't require signing.
## Restoring Built-in Windows New context menu
If the Windows 11 built-in New context menu doesn't reappear on uninstalling PowerToys, some issue with settings etc. here's how to restore the built-in New context menu.
1. Open Registry Editor
1. Go to the key "Computer\HKEY_CURRENT_USER\Software\Classes\Directory\background\ShellEx\ContextMenuHandlers"
1. Delete the "New" subkey (i.e. fullpath "Computer\HKEY_CURRENT_USER\Software\Classes\Directory\background\ShellEx\ContextMenuHandlers\New")

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View File

@@ -1119,6 +1119,35 @@ LExit:
return WcaFinalize(er);
}
UINT __stdcall RestoreBuiltInNewContextMenuCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
hr = WcaInitialize(hInstall, "RestoreBuiltInNewContextMenuCA");
constexpr wchar_t built_in_new_registry_path[] = LR"(Software\Classes\Directory\Background\ShellEx\ContextMenuHandlers\New)";
HKEY key{};
if (RegOpenKeyExW(HKEY_CURRENT_USER,
built_in_new_registry_path,
0,
KEY_ALL_ACCESS,
&key) != ERROR_SUCCESS)
{
return WcaFinalize(ERROR_SUCCESS);
}
if (RegDeleteValueW(key, nullptr) != ERROR_SUCCESS)
{
RegCloseKey(key);
return WcaFinalize(ERROR_SUCCESS);
}
RegCloseKey(key);
return WcaFinalize(ERROR_SUCCESS);
}
UINT __stdcall TelemetryLogInstallSuccessCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;

View File

@@ -7,6 +7,7 @@ EXPORTS
ApplyModulesRegistryChangeSetsCA
DetectPrevInstallPathCA
RemoveScheduledTasksCA
RestoreBuiltInNewContextMenuCA
TelemetryLogInstallSuccessCA
TelemetryLogInstallCancelCA
TelemetryLogInstallFailCA

View File

@@ -22,6 +22,16 @@
<ComponentGroup Id="DscResourcesComponentGroup">
<ComponentRef Id="PowerToysDSCReference" />
<?if $(var.PerUser) = "false" ?>
<Component Id="SecureDSCModulesFolder" Guid="7D2F4E57-CCB2-4F89-9B8B-62E9B3CC4E12" Directory="DSCModulesReferenceFolder" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="SecureDSCModulesFolder" Value="" KeyPath="yes" />
</RegistryKey>
<CreateFolder>
<PermissionEx Sddl="D:PAI(A;OICI;GA;;;SY)(A;OICI;GA;;;BA)(A;OICI;GRGX;;;BU)(A;OICIIO;GA;;;CO)" />
</CreateFolder>
</Component>
<?endif?>
<Component Id="RemoveDSCModulesFolder" Guid="A3C77D92-4E97-4C1A-9F2E-8B3C5D6E7F80" Directory="DSCModulesReferenceFolder">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemoveDSCModulesFolder" Value="" KeyPath="yes" />

View File

@@ -2,7 +2,29 @@
<?include $(sys.CURRENTDIR)\Common.wxi?>
<?define KeyboardManagerAssetsFiles=?>
<?define KeyboardManagerAssetsWinUI3Files=?>
<?define KeyboardManagerAssetsFilesPath=$(var.BinDir)\Assets\KeyboardManager\?>
<?define KeyboardManagerAssetsWinUI3FilesPath=$(var.BinDir)\WinUI3Apps\Assets\KeyboardManagerEditor\?>
<Fragment>
<DirectoryRef Id="BaseApplicationsAssetsFolder">
<Directory Id="KeyboardManagerAssetsInstallFolder" Name="KeyboardManager" />
</DirectoryRef>
<DirectoryRef Id="WinUI3AppsAssetsFolder">
<Directory Id="KeyboardManagerAssetsWinUI3InstallFolder" Name="KeyboardManagerEditor" />
</DirectoryRef>
<DirectoryRef Id="KeyboardManagerAssetsInstallFolder" FileSource="$(var.KeyboardManagerAssetsFilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--KeyboardManagerAssetsFiles_Component_Def-->
</DirectoryRef>
<DirectoryRef Id="KeyboardManagerAssetsWinUI3InstallFolder" FileSource="$(var.KeyboardManagerAssetsWinUI3FilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--KeyboardManagerAssetsWinUI3Files_Component_Def-->
</DirectoryRef>
<DirectoryRef Id="INSTALLFOLDER">
<Directory Id="KeyboardManagerEditorInstallFolder" Name="KeyboardManagerEditor" />
<Directory Id="KeyboardManagerEngineInstallFolder" Name="KeyboardManagerEngine" />
@@ -44,6 +66,8 @@
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemoveKeyboardManagerFolder" Value="" KeyPath="yes" />
</RegistryKey>
<RemoveFolder Id="RemoveFolderKeyboardManagerAssetsInstallFolder" Directory="KeyboardManagerAssetsInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderKeyboardManagerAssetsWinUI3InstallFolder" Directory="KeyboardManagerAssetsWinUI3InstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderKeyboardManagerEditorFolder" Directory="KeyboardManagerEditorInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderKeyboardManagerEngineFolder" Directory="KeyboardManagerEngineInstallFolder" On="uninstall" />
</Component>

View File

@@ -161,6 +161,9 @@
<!-- Clean Video Conference Mute registry keys that might be around from previous installations. We've deprecated this utility since then. -->
<Custom Action="CleanVideoConferenceRegistry" Before="InstallFinalize" Condition="NOT Installed" />
<!-- Restore built-in "New" context menu in case user disabled it via New+ -->
<Custom Action="RestoreBuiltInNewContextMenu" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot;)" />
</InstallExecuteSequence>
<CustomAction Id="SetLaunchPowerToysParam" Property="LaunchPowerToys" Value="[INSTALLFOLDER]" />
@@ -262,6 +265,8 @@
<CustomAction Id="SetBundleInstallLocation" Return="ignore" Impersonate="no" Execute="deferred" DllEntry="SetBundleInstallLocationCA" BinaryRef="PTCustomActions" />
<CustomAction Id="RestoreBuiltInNewContextMenu" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="RestoreBuiltInNewContextMenuCA" BinaryRef="PTCustomActions" />
<!-- Close 'PowerToys.exe' before uninstall-->
<Property Id="MSIRESTARTMANAGERCONTROL" Value="DisableShutdown" />
<Property Id="MSIFASTINSTALL" Value="DisableShutdown" />

View File

@@ -172,6 +172,12 @@ Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptR
Generate-FileList -fileDepsJson "" -fileListName ImageResizerAssetsFiles -wxsFilePath $PSScriptRoot\ImageResizer.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ImageResizer"
Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs
#KeyboardManager
Generate-FileList -fileDepsJson "" -fileListName KeyboardManagerAssetsFiles -wxsFilePath $PSScriptRoot\KeyboardManager.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\KeyboardManager"
Generate-FileList -fileDepsJson "" -fileListName KeyboardManagerAssetsWinUI3Files -wxsFilePath $PSScriptRoot\KeyboardManager.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\KeyboardManagerEditor"
Generate-FileComponents -fileListName "KeyboardManagerAssetsFiles" -wxsFilePath $PSScriptRoot\KeyboardManager.wxs
Generate-FileComponents -fileListName "KeyboardManagerAssetsWinUI3Files" -wxsFilePath $PSScriptRoot\KeyboardManager.wxs
# Light Switch Service
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs

View File

@@ -1,11 +1,11 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Microsoft.PowerToys.Common.UI.Controls">
xmlns:commoncontrols="using:Microsoft.PowerToys.Common.UI.Controls">
<Style BasedOn="{StaticResource DefaultKeyVisualStyle}" TargetType="local:KeyVisual" />
<Style BasedOn="{StaticResource DefaultKeyVisualStyle}" TargetType="commoncontrols:KeyVisual" />
<Style x:Key="DefaultKeyVisualStyle" TargetType="local:KeyVisual">
<Style x:Key="DefaultKeyVisualStyle" TargetType="commoncontrols:KeyVisual">
<Setter Property="MinWidth" Value="16" />
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
<Setter Property="IsTabStop" Value="False" />
@@ -25,7 +25,7 @@
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyVisual">
<ControlTemplate TargetType="commoncontrols:KeyVisual">
<Grid
x:Name="KeyHolder"
MinWidth="{TemplateBinding MinWidth}"
@@ -40,7 +40,7 @@
<Grid.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" />
</Grid.BackgroundTransition>
<local:KeyCharPresenter
<commoncontrols:KeyCharPresenter
x:Name="KeyPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
@@ -87,12 +87,12 @@
<Style
x:Key="SubtleKeyVisualStyle"
BasedOn="{StaticResource DefaultKeyVisualStyle}"
TargetType="local:KeyVisual">
TargetType="commoncontrols:KeyVisual">
<Setter Property="Background" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyVisual">
<ControlTemplate TargetType="commoncontrols:KeyVisual">
<Grid
x:Name="KeyHolder"
MinWidth="{TemplateBinding MinWidth}"
@@ -106,7 +106,7 @@
<Grid.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" />
</Grid.BackgroundTransition>
<local:KeyCharPresenter
<commoncontrols:KeyCharPresenter
x:Name="KeyPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
@@ -145,14 +145,14 @@
<Style
x:Key="AccentKeyVisualStyle"
BasedOn="{StaticResource DefaultKeyVisualStyle}"
TargetType="local:KeyVisual">
TargetType="commoncontrols:KeyVisual">
<Setter Property="Background" Value="{ThemeResource AccentFillColorDefaultBrush}" />
<Setter Property="Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource AccentControlElevationBorderBrush}" />
<Setter Property="BackgroundSizing" Value="OuterBorderEdge" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyVisual">
<ControlTemplate TargetType="commoncontrols:KeyVisual">
<Grid
x:Name="KeyHolder"
MinWidth="{TemplateBinding MinWidth}"
@@ -168,7 +168,7 @@
<Grid.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" />
</Grid.BackgroundTransition>
<local:KeyCharPresenter
<commoncontrols:KeyCharPresenter
x:Name="KeyPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

View File

@@ -292,4 +292,8 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredRunAtStartupValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredNewPlusHideBuiltInNewContextMenuValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredNewPlusHideBuiltInNewContextMenuValue());
}
}

View File

@@ -78,6 +78,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue();
static GpoRuleConfigured GetConfiguredNewPlusHideBuiltInNewContextMenuValue();
};
}

View File

@@ -82,6 +82,7 @@ namespace PowerToys
static GpoRuleConfigured GetAllowDataDiagnosticsValue();
static GpoRuleConfigured GetConfiguredRunAtStartupValue();
static GpoRuleConfigured GetConfiguredNewPlusReplaceVariablesValue();
static GpoRuleConfigured GetConfiguredNewPlusHideBuiltInNewContextMenuValue();
}
}
}

View File

@@ -287,4 +287,26 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::POWER_DISPLAY_TERMINATE_APP_MESSAGE;
}
hstring Constants::MWBToggleEasyMouseEvent()
{
return CommonSharedConstants::MWB_TOGGLE_EASY_MOUSE_EVENT;
}
hstring Constants::MWBReconnectEvent()
{
return CommonSharedConstants::MWB_RECONNECT_EVENT;
}
hstring Constants::OpenNewKeyboardManagerEvent()
{
return CommonSharedConstants::OPEN_NEW_KEYBOARD_MANAGER_EVENT;
}
hstring Constants::ToggleKeyboardManagerActiveEvent()
{
return CommonSharedConstants::TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT;
}
hstring Constants::KeyboardManagerEngineInstanceMutex()
{
return CommonSharedConstants::KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX;
}
}

View File

@@ -75,6 +75,11 @@ namespace winrt::PowerToys::Interop::implementation
static hstring PowerDisplayToggleMessage();
static hstring PowerDisplayApplyProfileMessage();
static hstring PowerDisplayTerminateAppMessage();
static hstring MWBToggleEasyMouseEvent();
static hstring MWBReconnectEvent();
static hstring OpenNewKeyboardManagerEvent();
static hstring ToggleKeyboardManagerActiveEvent();
static hstring KeyboardManagerEngineInstanceMutex();
};
}
@@ -84,3 +89,4 @@ namespace winrt::PowerToys::Interop::factory_implementation
{
};
}

View File

@@ -72,6 +72,12 @@ namespace PowerToys
static String PowerDisplayToggleMessage();
static String PowerDisplayApplyProfileMessage();
static String PowerDisplayTerminateAppMessage();
static String MWBToggleEasyMouseEvent();
static String MWBReconnectEvent();
static String OpenNewKeyboardManagerEvent();
static String ToggleKeyboardManagerActiveEvent();
static String KeyboardManagerEngineInstanceMutex();
}
}
}

View File

@@ -170,10 +170,20 @@ namespace CommonSharedConstants
const wchar_t LIGHT_SWITCH_LIGHT_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca";
const wchar_t LIGHT_SWITCH_DARK_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368";
// Path to events used by Keyboard Manager
const wchar_t OPEN_NEW_KEYBOARD_MANAGER_EVENT[] = L"Local\\PowerToysOpenNewKeyboardManagerEvent-9c1d2e3f-4b5a-6c7d-8e9f-0a1b2c3d4e5f";
const wchar_t TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT[] = L"Local\\PowerToysToggleKeyboardManagerActiveEvent-7f3a1d5c-2e94-4ff4-8b6a-90fd2bc4d2a7";
const wchar_t KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX[] = L"Local\\PowerToys_KBMEngine_InstanceMutex";
// used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";
// Path to the events used by MouseWithoutBorders
const wchar_t MWB_TOGGLE_EASY_MOUSE_EVENT[] = L"Local\\PowerToysMWB-ToggleEasyMouseEvent-a9c8d7b6-e5f4-3c2a-1b0d-9e8f7a6b5c4d";
const wchar_t MWB_RECONNECT_EVENT[] = L"Local\\PowerToysMWB-ReconnectEvent-b8d7c6a5-f4e3-2b1c-0a9d-8e7f6a5b4c3d";
// Max DWORD for key code to disable keys.
const DWORD VK_DISABLED = 0x100;
}

View File

@@ -103,6 +103,7 @@ namespace powertoys_gpo
const std::wstring POLICY_MWB_POLICY_DEFINED_IP_MAPPING_RULES = L"MwbPolicyDefinedIpMappingRules";
const std::wstring POLICY_NEW_PLUS_HIDE_TEMPLATE_FILENAME_EXTENSION = L"NewPlusHideTemplateFilenameExtension";
const std::wstring POLICY_NEW_PLUS_REPLACE_VARIABLES = L"NewPlusReplaceVariablesInTemplateFilenames";
const std::wstring POLICY_NEW_PLUS_HIDE_BUILT_IN_NEW_CONTEXT_MENU = L"NewPlusHideBuiltInNewContextMenu";
// Methods used for reading the registry
#pragma region ReadRegistryMethods
@@ -700,5 +701,10 @@ namespace powertoys_gpo
return getConfiguredValue(POLICY_NEW_PLUS_REPLACE_VARIABLES);
}
inline gpo_rule_configured_t getConfiguredNewPlusHideBuiltInNewContextMenuValue()
{
return getConfiguredValue(POLICY_NEW_PLUS_HIDE_BUILT_IN_NEW_CONTEXT_MENU);
}
#pragma endregion IndividualModuleSettingPolicies
}

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.19" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.20" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyNamespaces>
<target prefix="powertoys" namespace="Microsoft.Policies.PowerToys" />
</policyNamespaces>
<resources minRequiredRevision="1.19"/><!-- Last changed with PowerToys v0.97.0 -->
<resources minRequiredRevision="1.20"/><!-- Last changed with PowerToys v0.98.0 -->
<supportedOn>
<definitions>
<definition name="SUPPORTED_POWERTOYS_0_64_0" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0)"/>
@@ -28,6 +28,7 @@
<definition name="SUPPORTED_POWERTOYS_0_90_0" displayName="$(string.SUPPORTED_POWERTOYS_0_90_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_96_0" displayName="$(string.SUPPORTED_POWERTOYS_0_96_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_97_0" displayName="$(string.SUPPORTED_POWERTOYS_0_97_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_98_0" displayName="$(string.SUPPORTED_POWERTOYS_0_98_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1)"/>
</definitions>
</supportedOn>
@@ -826,5 +827,15 @@
<decimal value="0" />
</disabledValue>
</policy>
<policy name="NewPlusHideBuiltInNewContextMenu" class="Both" displayName="$(string.NewPlusHideBuiltInNewContextMenu)" explainText="$(string.NewPlusHideBuiltInNewContextMenuDescription)" key="Software\Policies\PowerToys" valueName="NewPlusHideBuiltInNewContextMenu">
<parentCategory ref="NewPlus" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_98_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
</policies>
</policyDefinitions>

View File

@@ -35,6 +35,7 @@
<string id="SUPPORTED_POWERTOYS_0_90_0">PowerToys version 0.90.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_96_0">PowerToys version 0.96.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_97_0">PowerToys version 0.97.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_98_0">PowerToys version 0.98.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1">From PowerToys version 0.64.0 until PowerToys version 0.87.1</string>
<string id="ConfigureAllUtilityGlobalEnabledStateDescription">This policy configures the enabled state for all PowerToys utilities.
@@ -238,7 +239,7 @@ If you disable this policy, the setting is disabled and variables in filenames w
If you don't configure this policy, the user will be able to control the setting and can enable or disable it.
</string>
<string id="ConfigureAllUtilityGlobalEnabledState">Configure global utility enabled state</string>
<string id="ConfigureEnabledUtilityAdvancedPaste">Advanced Paste: Configure enabled state</string>
<string id="ConfigureEnabledUtilityAlwaysOnTop">Always On Top: Configure enabled state</string>
@@ -356,6 +357,15 @@ If you disable this policy, users will not be able to select or use Foundry Loca
<string id="AllowDiagnosticData">Allow sending diagnostic data</string>
<string id="ConfigureRunAtStartup">Configure the run at startup setting</string>
<string id="NewPlusReplaceVariablesInTemplateFilenames">Replace variables in template filenames</string>
<string id="NewPlusHideBuiltInNewContextMenu">Hide the built-in "New" context menu</string>
<string id="NewPlusHideBuiltInNewContextMenuDescription">This policy configures if Windows' built-in New context menu should be hidden on the context menu.
If you enable this policy, then the built-in New context menu will be hidden, and user can only create new files and folders using New+ and the explorer toolbar New button.
If you disable this policy, then the build-in New context menu will be displayed as normal in Windows.
If you don't configure this policy, the user will be able to control the setting and can enable or disable it.
</string>
</stringTable>
<presentationTable>
@@ -369,4 +379,3 @@ If you disable this policy, users will not be able to select or use Foundry Loca
</resources>
</policyDefinitionResources>

View File

@@ -16,7 +16,6 @@ using System.Threading.Tasks;
using HostsUILib.Exceptions;
using HostsUILib.Models;
using HostsUILib.Settings;
using Microsoft.Win32;
namespace HostsUILib.Helpers
{
@@ -223,64 +222,17 @@ namespace HostsUILib.Helpers
public void OpenHostsFile()
{
var notepadFallback = false;
try
{
// Try to open in default editor
var key = Registry.ClassesRoot.OpenSubKey("SystemFileAssociations\\text\\shell\\edit\\command");
if (key != null)
{
var commandPattern = key.GetValue(string.Empty).ToString(); // Default value
var file = null as string;
var args = null as string;
if (commandPattern.StartsWith('\"'))
{
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
if (endQuoteIndex != -1)
{
file = commandPattern[1..endQuoteIndex];
args = commandPattern[(endQuoteIndex + 1)..].Trim();
}
}
else
{
var spaceIndex = commandPattern.IndexOf(' ');
if (spaceIndex != -1)
{
file = commandPattern[..spaceIndex];
args = commandPattern[(spaceIndex + 1)..].Trim();
}
}
if (file != null && args != null)
{
args = args.Replace("%1", HostsFilePath);
Process.Start(new ProcessStartInfo(file, args));
}
else
{
notepadFallback = true;
}
}
var notepadPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Windows),
"System32",
"notepad.exe");
Process.Start(new ProcessStartInfo(notepadPath, HostsFilePath));
}
catch (Exception ex)
{
LoggerInstance.Logger.LogError("Failed to open default editor", ex);
notepadFallback = true;
}
if (notepadFallback)
{
try
{
Process.Start(new ProcessStartInfo("notepad.exe", HostsFilePath));
}
catch (Exception ex)
{
LoggerInstance.Logger.LogError("Failed to open notepad", ex);
}
LoggerInstance.Logger.LogError("Failed to open notepad", ex);
}
}

View File

@@ -13,13 +13,11 @@
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
@@ -112,12 +110,8 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="COMPLETE_REWRITE_SUMMARY.md" />
<None Include="CRITICAL_BUG_ANALYSIS.md" />
<None Include="CURSOR_WRAP_FIX_ANALYSIS.md" />
<None Include="DEBUG_GUIDE.md" />
<None Include="CursorWrapTests\WrapSimulator\test_new_algorithm.py" />
<None Include="packages.config" />
<None Include="VERTICAL_WRAP_BUG_FIX.md" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
@@ -130,4 +124,4 @@
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>
</Project>

View File

@@ -163,6 +163,39 @@ void CursorWrapCore::UpdateMonitorInfo()
Logger::info(L"======= UPDATE MONITOR INFO END =======");
}
void CursorWrapCore::ResetWrapState()
{
m_hasPreviousPosition = false;
m_hasLastWrapDestination = false;
m_previousPosition = { LONG_MIN, LONG_MIN };
m_lastWrapDestination = { LONG_MIN, LONG_MIN };
}
CursorDirection CursorWrapCore::CalculateDirection(const POINT& currentPos) const
{
CursorDirection dir = { 0, 0 };
if (m_hasPreviousPosition)
{
dir.dx = currentPos.x - m_previousPosition.x;
dir.dy = currentPos.y - m_previousPosition.y;
}
return dir;
}
bool CursorWrapCore::IsWithinWrapThreshold(const POINT& currentPos) const
{
if (!m_hasLastWrapDestination)
{
return false;
}
int dx = currentPos.x - m_lastWrapDestination.x;
int dy = currentPos.y - m_lastWrapDestination.y;
int distanceSquared = dx * dx + dy * dy;
return distanceSquared <= (WRAP_DISTANCE_THRESHOLD * WRAP_DISTANCE_THRESHOLD);
}
POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode, bool disableOnSingleMonitor)
{
// Check if wrapping should be disabled on single monitor
@@ -176,6 +209,8 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
loggedOnce = true;
}
#endif
m_previousPosition = currentPos;
m_hasPreviousPosition = true;
return currentPos;
}
@@ -185,9 +220,31 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] [DRAG] Left mouse button down - skipping wrap\n");
#endif
m_previousPosition = currentPos;
m_hasPreviousPosition = true;
return currentPos;
}
// Check distance threshold to prevent rapid oscillation
if (IsWithinWrapThreshold(currentPos))
{
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] [THRESHOLD] Cursor within wrap threshold - skipping wrap\n");
#endif
m_previousPosition = currentPos;
m_hasPreviousPosition = true;
return currentPos;
}
// Clear wrap destination threshold once cursor moves away
if (m_hasLastWrapDestination && !IsWithinWrapThreshold(currentPos))
{
m_hasLastWrapDestination = false;
}
// Calculate cursor movement direction
CursorDirection direction = CalculateDirection(currentPos);
// Convert int wrapMode to WrapMode enum
WrapMode mode = static_cast<WrapMode>(wrapMode);
@@ -195,6 +252,7 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
{
std::wostringstream oss;
oss << L"[CursorWrap] [MOVE] Cursor at (" << currentPos.x << L", " << currentPos.y << L")";
oss << L" direction=(" << direction.dx << L", " << direction.dy << L")";
// Get current monitor and identify which one
HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST);
@@ -229,9 +287,9 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
// Get current monitor
HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST);
// Check if cursor is on an outer edge (filtered by wrap mode)
// Check if cursor is on an outer edge (filtered by wrap mode and direction)
EdgeType edgeType;
if (!m_topology.IsOnOuterEdge(currentMonitor, currentPos, edgeType, mode))
if (!m_topology.IsOnOuterEdge(currentMonitor, currentPos, edgeType, mode, &direction))
{
#ifdef _DEBUG
static bool lastWasNotOuter = false;
@@ -241,6 +299,8 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
lastWasNotOuter = true;
}
#endif
m_previousPosition = currentPos;
m_hasPreviousPosition = true;
return currentPos; // Not on an outer edge
}
@@ -278,5 +338,16 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
}
#endif
// Update tracking state
m_previousPosition = currentPos;
m_hasPreviousPosition = true;
// Store wrap destination for threshold checking
if (newPos.x != currentPos.x || newPos.y != currentPos.y)
{
m_lastWrapDestination = newPos;
m_hasLastWrapDestination = true;
}
return newPos;
}

View File

@@ -8,6 +8,24 @@
#include <string>
#include "MonitorTopology.h"
// Distance threshold to prevent rapid back-and-forth wrapping (in pixels)
constexpr int WRAP_DISTANCE_THRESHOLD = 50;
// Cursor movement direction
struct CursorDirection
{
int dx; // Horizontal movement (positive = right, negative = left)
int dy; // Vertical movement (positive = down, negative = up)
bool IsMovingLeft() const { return dx < 0; }
bool IsMovingRight() const { return dx > 0; }
bool IsMovingUp() const { return dy < 0; }
bool IsMovingDown() const { return dy > 0; }
// Returns true if horizontal movement is dominant
bool IsPrimarilyHorizontal() const { return abs(dx) >= abs(dy); }
};
// Core cursor wrapping engine
class CursorWrapCore
{
@@ -25,11 +43,28 @@ public:
size_t GetMonitorCount() const { return m_monitors.size(); }
const MonitorTopology& GetTopology() const { return m_topology; }
// Reset wrap state (call when disabling/re-enabling)
void ResetWrapState();
private:
#ifdef _DEBUG
std::wstring GenerateTopologyJSON() const;
#endif
// Calculate movement direction from previous position
CursorDirection CalculateDirection(const POINT& currentPos) const;
// Check if cursor is within threshold distance of last wrap position
bool IsWithinWrapThreshold(const POINT& currentPos) const;
std::vector<MonitorInfo> m_monitors;
MonitorTopology m_topology;
// Movement tracking for direction-based edge priority
POINT m_previousPosition = { LONG_MIN, LONG_MIN };
bool m_hasPreviousPosition = false;
// Wrap stability: prevent rapid oscillation
POINT m_lastWrapDestination = { LONG_MIN, LONG_MIN };
bool m_hasLastWrapDestination = false;
};

View File

@@ -0,0 +1,7 @@
<Solution>
<Configurations>
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Project Path="CursorLog/CursorLog.vcxproj" Id="646f6684-9f11-42cd-8b35-b2954404f985" />
</Solution>

View File

@@ -0,0 +1,196 @@
// CursorLog.cpp : Monitors mouse position and logs to file with monitor/DPI info
//
#include <iostream>
#include <fstream>
#include <string>
#include <filesystem>
#include <Windows.h>
#include <ShellScalingApi.h>
#pragma comment(lib, "Shcore.lib")
// Global variables
std::ofstream g_outputFile;
HHOOK g_mouseHook = nullptr;
POINT g_lastPosition = { LONG_MIN, LONG_MIN };
DWORD g_mainThreadId = 0;
// Get monitor information for a given point
std::string GetMonitorInfo(POINT pt, UINT* dpiX, UINT* dpiY)
{
HMONITOR hMonitor = MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST);
if (!hMonitor)
return "Unknown";
MONITORINFOEX monitorInfo = {};
monitorInfo.cbSize = sizeof(MONITORINFOEX);
GetMonitorInfo(hMonitor, &monitorInfo);
// Get DPI for this monitor
if (SUCCEEDED(GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI, dpiX, dpiY)))
{
// DPI retrieved successfully
}
else
{
*dpiX = 96;
*dpiY = 96;
}
// Convert device name to string using proper wide-to-narrow conversion
std::wstring deviceName(monitorInfo.szDevice);
int sizeNeeded = WideCharToMultiByte(CP_UTF8, 0, deviceName.c_str(), static_cast<int>(deviceName.length()), nullptr, 0, nullptr, nullptr);
std::string result(sizeNeeded, 0);
WideCharToMultiByte(CP_UTF8, 0, deviceName.c_str(), static_cast<int>(deviceName.length()), &result[0], sizeNeeded, nullptr, nullptr);
return result;
}
// Calculate scale factor from DPI
constexpr double GetScaleFactor(UINT dpi)
{
return static_cast<double>(dpi) / 96.0;
}
// Low-level mouse hook callback
LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION && wParam == WM_MOUSEMOVE)
{
MSLLHOOKSTRUCT* mouseStruct = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
POINT pt = mouseStruct->pt;
// Only log if position changed
if (pt.x != g_lastPosition.x || pt.y != g_lastPosition.y)
{
g_lastPosition = pt;
UINT dpiX = 96, dpiY = 96;
std::string monitorName = GetMonitorInfo(pt, &dpiX, &dpiY);
double scale = GetScaleFactor(dpiX);
if (g_outputFile.is_open())
{
g_outputFile << monitorName
<< "," << pt.x
<< "," << pt.y
<< "," << dpiX
<< "," << static_cast<int>(scale * 100) << "%"
<< "\n";
g_outputFile.flush();
}
}
}
return CallNextHookEx(g_mouseHook, nCode, wParam, lParam);
}
// Console control handler for clean shutdown
BOOL WINAPI ConsoleHandler(DWORD ctrlType)
{
if (ctrlType == CTRL_C_EVENT || ctrlType == CTRL_CLOSE_EVENT)
{
std::cout << "\nShutting down..." << std::endl;
if (g_mouseHook)
{
UnhookWindowsHookEx(g_mouseHook);
g_mouseHook = nullptr;
}
if (g_outputFile.is_open())
{
g_outputFile.close();
}
// Post quit message to the main thread to exit the message loop
PostThreadMessage(g_mainThreadId, WM_QUIT, 0, 0);
return TRUE;
}
return FALSE;
}
int main(int argc, char* argv[])
{
// Set DPI awareness FIRST, before any other Windows API calls
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
// Store main thread ID for clean shutdown
g_mainThreadId = GetCurrentThreadId();
// Check command line arguments
if (argc != 2)
{
std::cerr << "Usage: CursorLog.exe <output_path_and_filename>" << std::endl;
return 1;
}
std::filesystem::path outputPath(argv[1]);
std::filesystem::path parentPath = outputPath.parent_path();
// Validate the directory exists
if (!parentPath.empty() && !std::filesystem::exists(parentPath))
{
std::cerr << "Error: The directory '" << parentPath.string() << "' does not exist." << std::endl;
return 1;
}
// Check if file exists and prompt for overwrite
if (std::filesystem::exists(outputPath))
{
std::cout << "File '" << outputPath.string() << "' already exists. Overwrite? (y/n): ";
char response;
std::cin >> response;
if (response != 'y' && response != 'Y')
{
std::cout << "Operation cancelled." << std::endl;
return 0;
}
}
// Open output file
g_outputFile.open(outputPath, std::ios::out | std::ios::trunc);
if (!g_outputFile.is_open())
{
std::cerr << "Error: Unable to create or open file '" << outputPath.string() << "'." << std::endl;
return 1;
}
std::cout << "Logging mouse position to: " << outputPath.string() << std::endl;
std::cout << "Press Ctrl+C to stop..." << std::endl;
// Set up console control handler
SetConsoleCtrlHandler(ConsoleHandler, TRUE);
// Install low-level mouse hook
g_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, nullptr, 0);
if (!g_mouseHook)
{
std::cerr << "Error: Failed to install mouse hook. Error code: " << GetLastError() << std::endl;
g_outputFile.close();
return 1;
}
// Message loop - required for low-level hooks
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Cleanup
if (g_mouseHook)
{
UnhookWindowsHookEx(g_mouseHook);
}
if (g_outputFile.is_open())
{
g_outputFile.close();
}
return 0;
}

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>18.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{646f6684-9f11-42cd-8b35-b2954404f985}</ProjectGuid>
<RootNamespace>CursorLog</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard><PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard><PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard><PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard><PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="CursorLog.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="CursorLog.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,287 @@
# CursorWrap Simulator
A Python visualization tool that displays monitor layouts and shows which edges will wrap to other monitors using the exact same logic as the PowerToys CursorWrap implementation.
## Purpose
This tool helps you:
- Visualize your multi-monitor setup
- Identify which screen edges are "outer edges" (edges that don't connect to another monitor)
- See where cursor wrapping will occur when you move the cursor to an outer edge
- **Find problem areas** where edges have NO wrap destination (shown in red)
## Requirements
- Python 3.6+
- Tkinter (included with standard Python on Windows)
## Usage
### Command Line
```bash
python wrap_simulator.py <path_to_monitor_layout.json>
```
### Without Arguments
```bash
python wrap_simulator.py
```
This opens the application with no layout loaded. Use the "Load JSON" button to select a file.
## JSON File Format
The monitor layout JSON file should have this structure:
```json
{
"captured_at": "2026-02-16T08:50:34+00:00",
"computer_name": "MY-PC",
"user_name": "User",
"monitor_count": 3,
"monitors": [
{
"left": 0,
"top": 0,
"right": 2560,
"bottom": 1440,
"width": 2560,
"height": 1440,
"dpi": 96,
"scaling_percent": 100.0,
"primary": true,
"device_name": "DISPLAY1"
}
]
}
```
## Understanding the Visualization
### Monitor Display
- **Gray rectangles**: Individual monitors
- **Orange border**: Primary monitor
- **Labels**: Show monitor index, device name, and resolution
### Edge Bars (Outside Monitor Boundaries)
Colored bars are drawn outside each **outer edge** (edges not adjacent to another monitor):
| Color | Meaning |
|-------|---------|
| **Yellow** | Edge segment has a wrap destination ✓ |
| **Red with stripes** | NO wrap destination - Problem area! ⚠️ |
The bar outline color indicates the edge type:
- Red = Left edge
- Teal = Right edge
- Blue = Top edge
- Green = Bottom edge
### Interactive Features
1. **Hover over edge segments**:
- See wrap destination info in the status bar
- Green arrow shows where the cursor would wrap to
- Green dashed rectangle highlights the destination
2. **Click on edge segments**:
- Detailed information appears in the info panel
- Shows full problem analysis with reason codes
- Explains why wrapping does/doesn't occur
- Provides suggestions for fixing problems
3. **Wrap Mode Selection**:
- **Both**: Wrap in all directions (default)
- **Vertical Only**: Only top/bottom edges wrap
- **Horizontal Only**: Only left/right edges wrap
4. **Export Analysis**:
- Click "Export Analysis" to save detailed diagnostic data
- Exports to JSON format for use in algorithm development
- Includes all problem segments with reason codes and suggestions
5. **Edge Test Simulation** (NEW):
- Click "🧪 Test Edges" to start automated edge testing
- Visually animates cursor movement along ALL outer edges
- Shows wrap destination for each test point with colored lines:
- **Red circle**: Source position on outer edge
- **Green circle**: Wrap destination
- **Green dashed line**: Connection showing wrap path
- **Red X**: No wrap destination (problem area)
- Use "New Algorithm" checkbox to toggle between:
- **NEW**: Projection-based algorithm (eliminates dead zones)
- **OLD**: Direct overlap only (may have dead zones)
- Results summary shows per-edge coverage statistics
## Problem Analysis
When a segment has no wrap destination, the tool provides detailed analysis:
### Problem Reason Codes
| Code | Description |
|------|-------------|
| `WRAP_MODE_DISABLED` | Edge type disabled by current wrap mode setting |
| `NO_OPPOSITE_OUTER_EDGES` | No outer edges of the opposite type exist at all |
| `NO_OVERLAPPING_RANGE` | Opposite edges exist but don't cover this coordinate range |
| `SINGLE_MONITOR` | Only one monitor - nowhere to wrap to |
### Diagnostic Details
For `NO_OVERLAPPING_RANGE` problems, the tool shows:
- Distance to the nearest valid wrap destination
- List of available opposite edges sorted by distance
- Whether the gap is above/below or left/right of the segment
- Suggested fixes (extend monitors or adjust positions)
## Sample Files
Included sample layouts:
- `sample_layout.json` - 3 monitors in a row with one offset
- `sample_staggered.json` - 3 monitors with staggered vertical positions (shows problem areas)
- `sample_with_gap.json` - 2 monitors with a gap between them
## Exported Analysis Format
The "Export Analysis" button generates a JSON file with this structure:
```json
{
"export_timestamp": "2026-02-16T08:50:34+00:00",
"wrap_mode": "BOTH",
"monitor_count": 3,
"monitors": [...],
"outer_edges": [...],
"problem_segments": [
{
"source": {
"monitor_index": 0,
"monitor_name": "DISPLAY1",
"edge_type": "TOP",
"edge_position": 200,
"segment_range": {"start": 0, "end": 200},
"segment_length_px": 200
},
"analysis": {
"reason_code": "NO_OVERLAPPING_RANGE",
"description": "No BOTTOM outer edge overlaps...",
"suggestion": "To fix: Either extend...",
"details": {
"gap_to_nearest": 200,
"available_opposite_edges": [...]
}
}
}
],
"summary": {
"total_outer_edges": 8,
"total_problem_segments": 4,
"total_problem_pixels": 800,
"problems_by_reason": {"NO_OVERLAPPING_RANGE": 4},
"has_problems": true
}
}
```
## How CursorWrap Logic Works
### Original Algorithm (v1)
1. **Outer Edge Detection**: An edge is "outer" if no other monitor's opposite edge is within 50 pixels AND has sufficient vertical/horizontal overlap
2. **Wrap Destination**: When cursor reaches an outer edge:
- Find the opposite type outer edge (Left→Right, Top→Bottom, etc.)
- The destination must overlap with the cursor's perpendicular position
- Cursor warps to the furthest matching outer edge
3. **Problem Areas**: If no opposite outer edge overlaps with a portion of an outer edge, that segment has no wrap destination - the cursor will simply stop at that edge.
### Enhanced Algorithm (v2) - With Projection
The enhanced algorithm eliminates dead zones by projecting cursor positions to valid destinations:
1. **Direct Overlap**: If an opposite outer edge directly overlaps the cursor's perpendicular coordinate, use it (same as v1)
2. **Nearest Edge Projection**: If no direct overlap exists:
- Find the nearest opposite outer edge by coordinate distance
- Calculate a projected position using offset-from-boundary approach
- The projection preserves relative position similar to how Windows handles monitor transitions
3. **No Dead Zones**: Every point on every outer edge will have a valid wrap destination
### Testing the Algorithm
Use the included test script to validate both algorithms:
```bash
python test_new_algorithm.py [layout_file.json]
```
This compares the old algorithm (with dead zones) against the new algorithm (with projection) and reports coverage.
## Cursor Log Playback
The simulator can play back recorded cursor movement logs to visualize how the cursor moves across monitors.
### Loading a Cursor Log
1. Click "Load Log" to select a cursor movement log file
2. Use the playback controls:
- **▶ Play / ⏸ Pause**: Start or pause playback
- **⏹ Stop**: Stop and reset to beginning
- **⏮ Reset**: Reset to beginning without stopping
- **Speed slider**: Adjust playback speed (10-500ms between frames)
### Log File Format
The cursor log file is CSV format with the following columns:
```
display_name,x,y,dpi,scaling%
```
Example:
```csv
\\.\DISPLAY1,1234,567,96,100%
\\.\DISPLAY2,2560,720,144,150%
\\.\DISPLAY3,-500,800,96,100%
```
- **display_name**: Windows display name (e.g., `\\.\DISPLAY1`)
- **x, y**: Screen coordinates
- **dpi**: Display DPI
- **scaling%**: Display scaling percentage (with or without % sign)
Lines starting with `#` are treated as comments and ignored.
### Playback Visualization
- **Green cursor**: Normal movement within a monitor
- **Red cursor with burst effect**: Monitor transition detected
- **Blue trail**: Recent cursor movement path (fades over time)
- **Dashed red arrow**: Shows transition path between monitors
The playback automatically slows down when a monitor transition is detected, making it easier to observe wrap behavior.
### Sample Log File
A sample cursor log file `sample_cursor_log.csv` is included that demonstrates cursor movement across a three-monitor setup.
## Architecture
The Python implementation mirrors the C++ code structure:
- `MonitorTopology` class: Manages edge-based monitor layout
- `MonitorEdge` dataclass: Represents a single edge of a monitor
- `EdgeSegment` dataclass: A portion of an edge with wrap info
- `CursorLogEntry` dataclass: A single cursor movement log entry
- `WrapSimulatorApp`: Tkinter GUI application
## Integration with PowerToys
This tool is designed to validate and debug the CursorWrap feature. The JSON files can be generated by the debug build of CursorWrap or created manually for testing specific configurations.

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""
Test script to validate the new projection-based wrapping algorithm.
"""
import json
import sys
from wrap_simulator import MonitorTopology, MonitorInfo, WrapMode
def test_layout(layout_file: str):
"""Test a monitor layout with both old and new algorithms."""
# Load the layout
with open(layout_file, 'r') as f:
layout = json.load(f)
# Create monitor info objects
monitors = []
for i, m in enumerate(layout['monitors']):
monitors.append(MonitorInfo(
left=m['left'], top=m['top'], right=m['right'], bottom=m['bottom'],
width=m['width'], height=m['height'], dpi=m.get('dpi', 96),
scaling_percent=m.get('scaling_percent', 100), primary=m.get('primary', False),
device_name=m.get('device_name', f'DISPLAY{i+1}'), monitor_id=i
))
# Initialize topology
topology = MonitorTopology()
topology.initialize(monitors)
print(f"Layout: {layout_file}")
print(f"Monitors: {len(monitors)}")
print(f"Outer edges: {len(topology.outer_edges)}")
# Validate with OLD algorithm
print("\n--- OLD Algorithm (may have dead zones) ---")
old_problems = 0
old_problem_details = []
for edge in topology.outer_edges:
segments = topology.get_edge_segments_with_wrap_info(edge, WrapMode.BOTH)
for seg in segments:
if not seg.has_wrap_destination:
length = seg.end - seg.start
old_problems += length
detail = f"Mon {edge.monitor_index} {edge.edge_type.name} [{seg.start}-{seg.end}] ({length}px)"
old_problem_details.append(detail)
print(f" PROBLEM: {detail}")
print(f"Total problematic pixels: {old_problems}")
# Validate with NEW algorithm
print("\n--- NEW Algorithm (with projection) ---")
result = topology.validate_all_edges_have_destinations(WrapMode.BOTH)
print(f"Total edge length: {result['total_edge_length']}px")
print(f"Covered: {result['covered_length']}px ({result['coverage_percent']:.1f}%)")
print(f"Uncovered: {result['uncovered_length']}px")
print(f"Fully covered: {result['is_fully_covered']}")
if result['problem_areas']:
for prob in result['problem_areas']:
print(f" PROBLEM: {prob}")
# Summary
print("\n--- COMPARISON ---")
print(f"Old algorithm dead zones: {old_problems}px")
print(f"New algorithm dead zones: {result['uncovered_length']}px")
if old_problems > 0 and result['uncovered_length'] == 0:
print("SUCCESS: New algorithm eliminates all dead zones!")
elif result['uncovered_length'] > 0:
print("WARNING: New algorithm still has dead zones")
else:
print("Both algorithms have no dead zones for this layout")
return result['is_fully_covered']
def main():
layout_files = [
'mikehall_monitor_layout.json',
'sample_layout.json',
'sample_staggered.json',
]
# Allow specifying layout on command line
if len(sys.argv) > 1:
layout_files = sys.argv[1:]
all_passed = True
for layout_file in layout_files:
try:
print(f"\n{'='*60}")
passed = test_layout(layout_file)
if not passed:
all_passed = False
except FileNotFoundError:
print(f"File not found: {layout_file}")
except Exception as e:
print(f"Error testing {layout_file}: {e}")
all_passed = False
print(f"\n{'='*60}")
if all_passed:
print("ALL TESTS PASSED")
else:
print("SOME TESTS FAILED")
return 0 if all_passed else 1
if __name__ == "__main__":
sys.exit(main())

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
#include "pch.h"
#include "MonitorTopology.h"
#include "CursorWrapCore.h" // For CursorDirection struct
#include "../../../common/logger/logger.h"
#include <algorithm>
#include <cmath>
@@ -163,10 +164,80 @@ bool MonitorTopology::EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEd
int overlapStart = max(edge1.start, edge2.start);
int overlapEnd = min(edge1.end, edge2.end);
return overlapEnd > overlapStart + tolerance;
}
bool MonitorTopology::IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType, WrapMode wrapMode) const
EdgeType MonitorTopology::PrioritizeEdgeByDirection(const std::vector<EdgeType>& candidates,
const CursorDirection* direction) const
{
if (candidates.empty())
{
return EdgeType::Left; // Should not happen, but return a default
}
if (candidates.size() == 1 || direction == nullptr)
{
return candidates[0];
}
// Prioritize based on movement direction
// If moving primarily horizontally, prefer horizontal edges (Left/Right)
// If moving primarily vertically, prefer vertical edges (Top/Bottom)
if (direction->IsPrimarilyHorizontal())
{
// Prefer Left if moving left, Right if moving right
if (direction->IsMovingLeft())
{
for (EdgeType edge : candidates)
{
if (edge == EdgeType::Left) return edge;
}
}
else if (direction->IsMovingRight())
{
for (EdgeType edge : candidates)
{
if (edge == EdgeType::Right) return edge;
}
}
// Fall back to any horizontal edge
for (EdgeType edge : candidates)
{
if (edge == EdgeType::Left || edge == EdgeType::Right) return edge;
}
}
else
{
// Prefer Top if moving up, Bottom if moving down
if (direction->IsMovingUp())
{
for (EdgeType edge : candidates)
{
if (edge == EdgeType::Top) return edge;
}
}
else if (direction->IsMovingDown())
{
for (EdgeType edge : candidates)
{
if (edge == EdgeType::Bottom) return edge;
}
}
// Fall back to any vertical edge
for (EdgeType edge : candidates)
{
if (edge == EdgeType::Top || edge == EdgeType::Bottom) return edge;
}
}
// Default to first candidate
return candidates[0];
}
bool MonitorTopology::IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType,
WrapMode wrapMode, const CursorDirection* direction) const
{
RECT monitorRect;
if (!GetMonitorRect(monitor, monitorRect))
@@ -248,13 +319,40 @@ bool MonitorTopology::IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, Ed
return false;
}
// Try each candidate edge and return first with valid wrap destination
// Prioritize candidates by movement direction at corners
EdgeType prioritizedEdge = PrioritizeEdgeByDirection(candidateEdges, direction);
// Get the source edge info
auto sourceIt = m_edgeMap.find({monitorIndex, prioritizedEdge});
if (sourceIt == m_edgeMap.end())
{
return false;
}
// Use the new FindNearestOppositeEdge which handles non-overlapping regions
int cursorCoord = (prioritizedEdge == EdgeType::Left || prioritizedEdge == EdgeType::Right)
? cursorPos.y : cursorPos.x;
OppositeEdgeResult result = FindNearestOppositeEdge(prioritizedEdge, cursorCoord, sourceIt->second);
if (result.found)
{
outEdgeType = prioritizedEdge;
return true;
}
// If prioritized edge didn't work, try other candidates
for (EdgeType candidate : candidateEdges)
{
MonitorEdge oppositeEdge = FindOppositeOuterEdge(candidate,
(candidate == EdgeType::Left || candidate == EdgeType::Right) ? cursorPos.y : cursorPos.x);
if (oppositeEdge.monitorIndex >= 0)
if (candidate == prioritizedEdge) continue;
auto it = m_edgeMap.find({monitorIndex, candidate});
if (it == m_edgeMap.end()) continue;
int coord = (candidate == EdgeType::Left || candidate == EdgeType::Right)
? cursorPos.y : cursorPos.x;
OppositeEdgeResult altResult = FindNearestOppositeEdge(candidate, coord, it->second);
if (altResult.found)
{
outEdgeType = candidate;
return true;
@@ -280,16 +378,14 @@ POINT MonitorTopology::GetWrapDestination(HMONITOR fromMonitor, const POINT& cur
}
const MonitorEdge& fromEdge = it->second;
// Get cursor coordinate perpendicular to the edge
int cursorCoord = (edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x;
// Calculate relative position on current edge (0.0 to 1.0)
double relativePos = GetRelativePosition(fromEdge,
(edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x);
// Use the new FindNearestOppositeEdge which handles non-overlapping regions
OppositeEdgeResult oppositeResult = FindNearestOppositeEdge(edgeType, cursorCoord, fromEdge);
// Find opposite outer edge
MonitorEdge oppositeEdge = FindOppositeOuterEdge(edgeType,
(edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x);
if (oppositeEdge.monitorIndex < 0)
if (!oppositeResult.found)
{
// No opposite edge found, wrap within same monitor
RECT monitorRect;
@@ -321,15 +417,35 @@ POINT MonitorTopology::GetWrapDestination(HMONITOR fromMonitor, const POINT& cur
if (edgeType == EdgeType::Left || edgeType == EdgeType::Right)
{
// Horizontal edge -> vertical movement
result.x = oppositeEdge.position;
result.y = GetAbsolutePosition(oppositeEdge, relativePos);
// Horizontal wrapping (Left<->Right edges)
result.x = oppositeResult.edge.position;
if (oppositeResult.requiresProjection)
{
// Use the pre-calculated projected coordinate for non-overlapping regions
result.y = oppositeResult.projectedCoordinate;
}
else
{
// Overlapping region - preserve Y coordinate
result.y = cursorPos.y;
}
}
else
{
// Vertical edge -> horizontal movement
result.y = oppositeEdge.position;
result.x = GetAbsolutePosition(oppositeEdge, relativePos);
// Vertical wrapping (Top<->Bottom edges)
result.y = oppositeResult.edge.position;
if (oppositeResult.requiresProjection)
{
// Use the pre-calculated projected coordinate for non-overlapping regions
result.x = oppositeResult.projectedCoordinate;
}
else
{
// Overlapping region - preserve X coordinate
result.x = cursorPos.x;
}
}
return result;
@@ -387,6 +503,170 @@ MonitorEdge MonitorTopology::FindOppositeOuterEdge(EdgeType fromEdge, int relati
return result;
}
OppositeEdgeResult MonitorTopology::FindNearestOppositeEdge(EdgeType fromEdge, int cursorCoordinate,
const MonitorEdge& sourceEdge) const
{
OppositeEdgeResult result;
result.found = false;
result.requiresProjection = false;
result.projectedCoordinate = 0;
result.edge.monitorIndex = -1;
EdgeType targetType;
bool findMax; // true = find max position (furthest right/bottom), false = find min (furthest left/top)
switch (fromEdge)
{
case EdgeType::Left:
targetType = EdgeType::Right;
findMax = true;
break;
case EdgeType::Right:
targetType = EdgeType::Left;
findMax = false;
break;
case EdgeType::Top:
targetType = EdgeType::Bottom;
findMax = true;
break;
case EdgeType::Bottom:
targetType = EdgeType::Top;
findMax = false;
break;
default:
return result; // Invalid edge type
}
// First, try to find an edge that directly overlaps the cursor coordinate
MonitorEdge directMatch = FindOppositeOuterEdge(fromEdge, cursorCoordinate);
if (directMatch.monitorIndex >= 0)
{
result.found = true;
result.requiresProjection = false;
result.edge = directMatch;
result.projectedCoordinate = cursorCoordinate; // Not used, but set for completeness
return result;
}
// No direct overlap - find the nearest opposite edge by coordinate distance
// This handles the "dead zone" case where cursor is in a non-overlapping region
int bestDistance = INT_MAX;
MonitorEdge bestEdge = { .monitorIndex = -1 };
int bestProjectedCoord = 0;
for (const auto& edge : m_outerEdges)
{
if (edge.type != targetType)
{
continue;
}
// Calculate distance from cursor coordinate to this edge's range
int distance = 0;
int projectedCoord = 0;
if (cursorCoordinate < edge.start)
{
// Cursor is before the edge's start - project to edge start with offset
distance = edge.start - cursorCoordinate;
projectedCoord = edge.start; // Clamp to edge start
}
else if (cursorCoordinate > edge.end)
{
// Cursor is after the edge's end - project to edge end with offset
distance = cursorCoordinate - edge.end;
projectedCoord = edge.end; // Clamp to edge end
}
else
{
// Cursor overlaps - this shouldn't happen since we checked direct match
distance = 0;
projectedCoord = cursorCoordinate;
}
// Choose the best edge: prefer closer edges, and among equals prefer extreme position
bool isBetter = false;
if (distance < bestDistance)
{
isBetter = true;
}
else if (distance == bestDistance && bestEdge.monitorIndex >= 0)
{
// Same distance - prefer the extreme position (furthest in wrap direction)
if ((findMax && edge.position > bestEdge.position) ||
(!findMax && edge.position < bestEdge.position))
{
isBetter = true;
}
}
if (isBetter)
{
bestDistance = distance;
bestEdge = edge;
bestProjectedCoord = projectedCoord;
}
}
if (bestEdge.monitorIndex >= 0)
{
result.found = true;
result.requiresProjection = true;
result.edge = bestEdge;
// Calculate projected position using offset-from-boundary approach
result.projectedCoordinate = CalculateProjectedPosition(cursorCoordinate, sourceEdge, bestEdge);
Logger::trace(L"FindNearestOppositeEdge: Non-overlapping wrap from {} to Mon {} edge, cursor={}, projected={}",
static_cast<int>(fromEdge), bestEdge.monitorIndex, cursorCoordinate, result.projectedCoordinate);
}
return result;
}
int MonitorTopology::CalculateProjectedPosition(int cursorCoordinate, const MonitorEdge& sourceEdge,
const MonitorEdge& targetEdge) const
{
// Windows behavior for non-overlapping regions:
// When cursor is in a region that doesn't overlap with the target edge,
// clamp to the nearest boundary of the target edge.
// This matches observed Windows cursor transition behavior.
// Find the shared boundary region between source and target edges
int sharedStart = max(sourceEdge.start, targetEdge.start);
int sharedEnd = min(sourceEdge.end, targetEdge.end);
if (cursorCoordinate >= sharedStart && cursorCoordinate <= sharedEnd)
{
// Cursor is in shared region - preserve the coordinate exactly
return cursorCoordinate;
}
// For non-overlapping regions, clamp to the nearest boundary of the target edge
// This matches Windows behavior where the cursor is projected to the closest
// valid point on the destination edge
int projectedCoord;
if (cursorCoordinate < sharedStart)
{
// Cursor is BEFORE the shared region (e.g., above shared area)
// Clamp to the start of the target edge (with small offset to stay within bounds)
projectedCoord = targetEdge.start + 1;
}
else
{
// Cursor is AFTER the shared region (e.g., below shared area)
// Clamp to the end of the target edge (with small offset to stay within bounds)
projectedCoord = targetEdge.end - 1;
}
// Final bounds check
projectedCoord = max(targetEdge.start, min(projectedCoord, targetEdge.end - 1));
return projectedCoord;
}
double MonitorTopology::GetRelativePosition(const MonitorEdge& edge, int coordinate) const
{
if (edge.end == edge.start)

View File

@@ -7,6 +7,9 @@
#include <vector>
#include <map>
// Forward declaration
struct CursorDirection;
// Monitor information structure
struct MonitorInfo
{
@@ -44,6 +47,15 @@ struct MonitorEdge
bool isOuter; // True if no adjacent monitor touches this edge
};
// Result of finding an opposite edge, including projection info for non-overlapping regions
struct OppositeEdgeResult
{
MonitorEdge edge;
bool found; // True if an opposite edge was found
bool requiresProjection; // True if cursor position needs to be projected (non-overlapping region)
int projectedCoordinate; // The calculated coordinate on the target edge
};
// Monitor topology helper - manages edge-based monitor layout
struct MonitorTopology
{
@@ -51,7 +63,9 @@ struct MonitorTopology
// Check if cursor is on an outer edge of the given monitor
// wrapMode filters which edges are considered (Both, VerticalOnly, HorizontalOnly)
bool IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType, WrapMode wrapMode) const;
// direction is used to prioritize edges at corners based on cursor movement
bool IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType,
WrapMode wrapMode, const CursorDirection* direction = nullptr) const;
// Get the wrap destination point for a cursor on an outer edge
POINT GetWrapDestination(HMONITOR fromMonitor, const POINT& cursorPos, EdgeType edgeType) const;
@@ -95,12 +109,26 @@ private:
// Check if two edges are adjacent (within tolerance)
bool EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEdge& edge2, int tolerance = 50) const;
// Find the opposite outer edge for wrapping
// Find the opposite outer edge for wrapping (original method - for overlapping regions)
MonitorEdge FindOppositeOuterEdge(EdgeType fromEdge, int relativePosition) const;
// Find the nearest opposite outer edge, including projection for non-overlapping regions
// This implements Windows-like behavior for cursor transitions
OppositeEdgeResult FindNearestOppositeEdge(EdgeType fromEdge, int cursorCoordinate,
const MonitorEdge& sourceEdge) const;
// Calculate projected position for cursor in non-overlapping region
// Returns the coordinate on the destination edge using offset-from-boundary approach
int CalculateProjectedPosition(int cursorCoordinate, const MonitorEdge& sourceEdge,
const MonitorEdge& targetEdge) const;
// Calculate relative position along an edge (0.0 to 1.0)
double GetRelativePosition(const MonitorEdge& edge, int coordinate) const;
// Convert relative position to absolute coordinate on target edge
int GetAbsolutePosition(const MonitorEdge& edge, double relativePosition) const;
// Prioritize edge candidates based on cursor movement direction
EdgeType PrioritizeEdgeByDirection(const std::vector<EdgeType>& candidates,
const CursorDirection* direction) const;
};

View File

@@ -54,6 +54,7 @@ namespace
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t JSON_KEY_DISABLE_WRAP_DURING_DRAG[] = L"disable_wrap_during_drag";
const wchar_t JSON_KEY_WRAP_MODE[] = L"wrap_mode";
const wchar_t JSON_KEY_ACTIVATION_MODE[] = L"activation_mode";
const wchar_t JSON_KEY_DISABLE_ON_SINGLE_MONITOR[] = L"disable_cursor_wrap_on_single_monitor";
}
@@ -83,6 +84,7 @@ private:
bool m_disableWrapDuringDrag = true; // Default to true to prevent wrap during drag
bool m_disableOnSingleMonitor = false; // Default to false
int m_wrapMode = 0; // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
int m_activationMode = 0; // 0=Always (default), 1=HoldingCtrl (wraps only while held), 2=HoldingShift (wraps only while held)
// Mouse hook
HHOOK m_mouseHook = nullptr;
@@ -430,6 +432,21 @@ private:
Logger::warn("Failed to initialize CursorWrap wrap mode from settings. Will use default value (0=Both)");
}
try
{
// Parse activation mode
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
if (propertiesObject.HasKey(JSON_KEY_ACTIVATION_MODE))
{
auto activationModeObject = propertiesObject.GetNamedObject(JSON_KEY_ACTIVATION_MODE);
m_activationMode = static_cast<int>(activationModeObject.GetNamedNumber(JSON_KEY_VALUE));
}
}
catch (...)
{
Logger::warn("Failed to initialize CursorWrap activation mode from settings. Will use default value (0=Always)");
}
try
{
// Parse disable on single monitor
@@ -672,6 +689,26 @@ private:
if (g_cursorWrapInstance && g_cursorWrapInstance->m_hookActive)
{
// Check activation mode to determine if wrapping should happen.
// 0=Always, 1=HoldingCtrl (wraps only when Ctrl held), 2=HoldingShift (wraps only when Shift held)
int activationMode = g_cursorWrapInstance->m_activationMode;
bool shouldWrap = true;
if (activationMode == 1) // HoldingCtrl - wrap only when Ctrl is held
{
shouldWrap = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
}
else if (activationMode == 2) // HoldingShift - wrap only when Shift is held
{
shouldWrap = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
}
if (!shouldWrap)
{
// Activation key is not held, do not wrap - let normal behavior happen.
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
POINT newPos = g_cursorWrapInstance->m_core.HandleMouseMove(
currentPos,
g_cursorWrapInstance->m_disableWrapDuringDrag,

View File

@@ -229,6 +229,7 @@ namespace MouseWithoutBorders.Class
if (!Common.RunOnLogonDesktop)
{
StartSettingSyncThread();
CommandEventHandler.StartListening();
}
Application.EnableVisualStyles();

View File

@@ -0,0 +1,114 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using MouseWithoutBorders.Class;
using PowerToys.Interop;
namespace MouseWithoutBorders.Core
{
/// <summary>
/// Handles command events from external sources (e.g., Command Palette).
/// Uses named events for inter-process communication, following the same pattern as other PowerToys modules.
/// </summary>
internal static class CommandEventHandler
{
private static CancellationTokenSource _cancellationTokenSource;
/// <summary>
/// Starts listening for command events on background threads.
/// </summary>
public static void StartListening()
{
_cancellationTokenSource = new CancellationTokenSource();
CancellationToken exitToken = _cancellationTokenSource.Token;
// Start listener for Toggle Easy Mouse event
StartEventListener(Constants.MWBToggleEasyMouseEvent(), ToggleEasyMouse, exitToken);
// Start listener for Reconnect event
StartEventListener(Constants.MWBReconnectEvent(), Reconnect, exitToken);
}
/// <summary>
/// Stops listening for command events.
/// </summary>
public static void StopListening()
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
private static void StartEventListener(string eventName, Action callback, CancellationToken cancel)
{
new System.Threading.Thread(() =>
{
try
{
using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
WaitHandle[] waitHandles = new WaitHandle[] { cancel.WaitHandle, eventHandle };
while (!cancel.IsCancellationRequested)
{
int result = WaitHandle.WaitAny(waitHandles);
if (result == 1)
{
// Execute callback on UI thread using Common.DoSomethingInUIThread
Common.DoSomethingInUIThread(callback);
}
else
{
// Cancellation requested
return;
}
}
}
catch (Exception ex)
{
Logger.Log($"Error in event listener for {eventName}: {ex.Message}");
}
})
{ IsBackground = true, Name = $"MWB-{eventName}-Listener" }.Start();
}
/// <summary>
/// Toggles Easy Mouse between Enabled and Disabled states.
/// This is the same logic used by the hotkey handler.
/// </summary>
public static void ToggleEasyMouse()
{
if (Common.RunOnLogonDesktop || Common.RunOnScrSaverDesktop)
{
return;
}
EasyMouseOption easyMouseOption = (EasyMouseOption)Setting.Values.EasyMouse;
if (easyMouseOption is EasyMouseOption.Disable or EasyMouseOption.Enable)
{
Setting.Values.EasyMouse = (int)(easyMouseOption == EasyMouseOption.Disable ? EasyMouseOption.Enable : EasyMouseOption.Disable);
Common.ShowToolTip($"Easy Mouse has been toggled to [{(EasyMouseOption)Setting.Values.EasyMouse}].", 3000);
Logger.Log($"Easy Mouse toggled to {(EasyMouseOption)Setting.Values.EasyMouse} via command event.");
}
}
/// <summary>
/// Initiates a reconnection attempt to all machines.
/// This is the same logic used by the hotkey handler.
/// </summary>
public static void Reconnect()
{
Common.ShowToolTip("Reconnecting...", 2000);
Common.LastReconnectByHotKeyTime = Common.GetTick();
InitAndCleanup.PleaseReopenSocket = InitAndCleanup.REOPEN_WHEN_HOTKEY;
Logger.Log("Reconnect initiated via command event.");
}
}
}

View File

@@ -218,6 +218,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -18,6 +18,8 @@ namespace newplus::constants::non_localizable
constexpr WCHAR settings_json_key_template_location[] = L"TemplateLocation";
constexpr WCHAR settings_json_key_hide_built_in_new[] = L"BuiltInNewHidePreference";
constexpr WCHAR context_menu_package_name[] = L"NewPlusContextMenu";
constexpr WCHAR msix_package_name[] = L"NewPlusPackage.msix";

View File

@@ -4,22 +4,6 @@
namespace newplus::helpers::filesystem
{
namespace constants::non_localizable
{
constexpr WCHAR desktop_ini_filename[] = L"desktop.ini";
}
inline bool is_hidden(const std::filesystem::path path)
{
const std::filesystem::path::string_type name = path.filename();
if (name == constants::non_localizable::desktop_ini_filename)
{
return true;
}
return false;
}
inline bool is_directory(const std::filesystem::path path)
{
const auto entry = std::filesystem::directory_entry(path);

View File

@@ -129,6 +129,18 @@ namespace newplus::helpers::variables
return result;
}
static bool exclude_item(const std::filesystem::path& path)
{
DWORD attrs = GetFileAttributesW(path.c_str());
if (attrs == INVALID_FILE_ATTRIBUTES)
{
return false;
}
// Exclude if hidden or system
return (attrs & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)) != 0;
}
inline void resolve_variables_in_filename_and_rename_files(const std::filesystem::path& path, const bool do_rename = true)
{
// Depth first recursion, so that we start renaming the leaves, and avoid having to rescan
@@ -143,7 +155,7 @@ namespace newplus::helpers::variables
// Perform the actual rename
for (const auto& current : std::filesystem::directory_iterator(path))
{
if (!newplus::helpers::filesystem::is_hidden(current))
if (!exclude_item(current))
{
const std::filesystem::path resolved_path = resolve_variables_in_path(current.path());

View File

@@ -446,4 +446,69 @@ namespace newplus::utilities
return hr;
}
constexpr wchar_t built_in_new_registry_path[] = LR"(Software\Classes\Directory\Background\ShellEx\ContextMenuHandlers\New)";
constexpr wchar_t built_in_new_registry_disabled_value_prefix[] = L"disabled_";
inline bool disable_built_in_new_via_registry()
{
// This is implemented to support where New+ GPO is configured to
// hide the built-in New context menu but Settings UI hasn't been launched
// Mirrors the logic in DisableBuiltInNewViaRegistry in .cs
HKEY key{};
if (RegCreateKeyExW(HKEY_CURRENT_USER,
built_in_new_registry_path,
0,
nullptr,
REG_OPTION_NON_VOLATILE,
KEY_ALL_ACCESS,
nullptr,
&key,
nullptr) != ERROR_SUCCESS)
{
return false;
}
const auto built_in_new_registry_disabled_value_prefix_len = lstrlenW(built_in_new_registry_disabled_value_prefix);
if (RegSetValueExW(key, nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(&built_in_new_registry_disabled_value_prefix), built_in_new_registry_disabled_value_prefix_len) != ERROR_SUCCESS)
{
RegCloseKey(key);
return true;
}
RegCloseKey(key);
return false;
}
inline bool enable_built_in_new_via_registry()
{
// This is implemented to support where New+ GPO is configured to
// display the built-in New context menu but Settings UI hasn't been launched
// Mirrors the logic in EnableBuiltInNewViaRegistry in .cs
HKEY key{};
if (RegOpenKeyExW(HKEY_CURRENT_USER,
built_in_new_registry_path,
0,
KEY_ALL_ACCESS,
&key) != ERROR_SUCCESS)
{
return true;
}
if (RegDeleteValueW(key, nullptr) != ERROR_SUCCESS)
{
RegCloseKey(key);
return true;
}
RegCloseKey(key);
return false;
}
}

View File

@@ -172,7 +172,22 @@ private:
void init_settings()
{
powertoy_new_enabled = NewSettingsInstance().GetEnabled();
UpdateRegistration(powertoy_new_enabled);
if (powertoy_new_enabled)
{
// NOTE: This requires that the runner is running and have loaded the new plus module.
// It's not enough for user to just invoke the context menu.
if (NewSettingsInstance().GetHideBuiltInNew())
{
newplus::utilities::disable_built_in_new_via_registry();
}
else
{
newplus::utilities::enable_built_in_new_via_registry();
}
}
}
};

View File

@@ -45,6 +45,7 @@ void NewSettings::Save()
values.add_property(newplus::constants::non_localizable::settings_json_key_hide_starting_digits, new_settings.hide_starting_digits);
values.add_property(newplus::constants::non_localizable::settings_json_key_replace_variables, new_settings.replace_variables);
values.add_property(newplus::constants::non_localizable::settings_json_key_template_location, new_settings.template_location);
values.add_property(newplus::constants::non_localizable::settings_json_key_hide_built_in_new, new_settings.hide_built_in_new_preference);
values.save_to_settings_file();
@@ -75,6 +76,9 @@ void NewSettings::InitializeWithDefaultSettings()
SetReplaceVariables(false);
SetTemplateLocation(GetTemplateLocationDefaultPath());
// By default we show the built-in New context menu
SetHideBuiltInNew(false);
}
void NewSettings::RefreshEnabledState()
@@ -149,6 +153,12 @@ void NewSettings::ParseJson()
new_settings.replace_variables = resolveVariables.value();
}
const auto hideBuiltInNewValue = settings.get_bool_value(newplus::constants::non_localizable::settings_json_key_hide_built_in_new);
if (hideBuiltInNewValue.has_value())
{
new_settings.hide_built_in_new_preference = hideBuiltInNewValue.value();
}
GetSystemTimeAsFileTime(&new_settings_last_loaded_timestamp);
}
@@ -239,6 +249,26 @@ std::wstring NewSettings::GetTemplateLocationDefaultPath() const
return full_path;
}
bool NewSettings::GetHideBuiltInNew()
{
const auto gpoSetting = powertoys_gpo::getConfiguredNewPlusHideBuiltInNewContextMenuValue();
if (gpoSetting == powertoys_gpo::gpo_rule_configured_enabled)
{
return true;
}
else if (gpoSetting == powertoys_gpo::gpo_rule_configured_disabled)
{
return false;
}
return new_settings.hide_built_in_new_preference;
}
void NewSettings::SetHideBuiltInNew(const bool hide_built_in_new)
{
new_settings.hide_built_in_new_preference = hide_built_in_new;
}
NewSettings& NewSettingsInstance()
{
static NewSettings instance;

View File

@@ -16,6 +16,8 @@ public:
void SetReplaceVariables(const bool resolve_variables);
std::wstring GetTemplateLocation() const;
void SetTemplateLocation(const std::wstring template_location);
bool GetHideBuiltInNew();
void SetHideBuiltInNew(const bool hide_built_in_new);
void Save();
void Load();
@@ -29,6 +31,7 @@ private:
bool hide_starting_digits{ true };
bool replace_variables{ true };
std::wstring template_location;
bool hide_built_in_new_preference{ false };
};
void RefreshEnabledState();

View File

@@ -35,7 +35,7 @@ void template_folder::rescan_template_folder()
}
else
{
if (!helpers::filesystem::is_hidden(entry.path()))
if (!newplus::helpers::variables::exclude_item(entry.path()))
{
files.push_back({ entry.path().wstring(), new template_item(entry) });
}

View File

@@ -1,5 +1,3 @@
#include "pch.h"
#include "template_item.h"
#include <shellapi.h>
@@ -60,10 +58,91 @@ std::wstring template_item::get_target_filename(const bool include_starting_digi
std::wstring template_item::remove_starting_digits_from_filename(std::wstring filename) const
{
filename.erase(0, std::min(filename.find_first_not_of(L"0123456789"), filename.size()));
filename.erase(0, std::min(filename.find_first_not_of(L" ."), filename.size()));
// Filename cases to support
// type | filename | result
// [file] | 01. First entry.txt | First entry.txt
// [folder] | 02. Second entry | Second entry
// [folder] | 03 Third entry | Third entry
// [file] | 04 Fourth entry.txt | Fourth entry.txt
// [file] | 05.Fifth entry.txt | Fifth entry.txt
// [folder] | 001231 | 001231
// [file] | 001231.txt | 001231.txt
// [file] | 13. 0123456789012345.txt | 0123456789012345.txt
return filename;
std::filesystem::path filename_path(filename);
const std::wstring stem = filename_path.stem().wstring();
bool stem_is_only_digits = !stem.empty();
for (const wchar_t c : stem)
{
if (c < L'0' || c > L'9')
{
stem_is_only_digits = false;
break;
}
}
if (stem_is_only_digits)
{
// Edge cases where digits ARE the filename.
// If it's a file, we always keep it (e.g. 001231.txt or 001231).
// If it's a folder, we only strip if it looks like it has an extension (which is actually part of the name for folders).
// e.g. "0123.Name" -> Strip. "001231" -> Keep.
const bool is_folder = helpers::filesystem::is_directory(path);
const bool has_extension = filename_path.has_extension();
if (!is_folder || !has_extension)
{
return filename;
}
}
// Find end of leading digits
size_t digits_end_index = 0;
while (digits_end_index < filename.length() && filename[digits_end_index] >= L'0' && filename[digits_end_index] <= L'9')
{
digits_end_index++;
}
if (digits_end_index == 0)
{
// No leading digits
return filename;
}
// Determine if we should also strip a separator (dot or space)
size_t strip_length = digits_end_index;
// Check patterns to strip separators:
// 1. "01. Name" -> Strip "01. "
// 2. "01 .Name" -> Strip "01 ."
// 3. "01.Name" -> Strip "01."
// 4. "01 Name" -> Strip "01 "
// 5. "01Name" -> Strip "01" (No separator)
if (strip_length < filename.length())
{
if (filename[strip_length] == L'.')
{
strip_length++;
// If dot is followed by space, strip that too (e.g. "01. Name")
if (strip_length < filename.length() && filename[strip_length] == L' ')
{
strip_length++;
}
}
else if (filename[strip_length] == L' ')
{
strip_length++;
// If space is followed by dot, strip that too (e.g. "01 .Name")
if (strip_length < filename.length() && filename[strip_length] == L'.')
{
strip_length++;
}
}
}
return filename.substr(strip_length);
}
std::wstring template_item::get_explorer_icon() const

View File

@@ -5679,6 +5679,16 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
// Recording completed (closed via hotkey or item close). Proceed to save/trim workflow.
OutputDebugStringW(L"[Recording] StartAsync completed, entering save workflow\n");
// Release the writer stream and session objects before trim/save. Keeping the temp file
// open here can cause trimming and later MoveAndReplaceAsync calls to fail on the same file.
if (stream)
{
stream.Close();
stream = nullptr;
}
g_RecordingSession = nullptr;
g_GifRecordingSession = nullptr;
// Resume on the UI thread for the save dialog
co_await uiThread;
OutputDebugStringW(L"[Recording] Resumed on UI thread\n");

View File

@@ -22,6 +22,7 @@ namespace NonLocalizable
const static wchar_t* TOOL_WINDOW_CLASS_NAME = L"AlwaysOnTopWindow";
const static wchar_t* WINDOW_IS_PINNED_PROP = L"AlwaysOnTop_Pinned";
constexpr UINT SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND = 0xEFE0;
constexpr ULONG_PTR SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND_OWNER_TAG = 0x414F5450;
constexpr DWORD SYSTEM_EVENT_MENU_POPUP_START = 0x0006;
constexpr DWORD SYSTEM_EVENT_MENU_POPUP_END = 0x0007;
}
@@ -40,6 +41,29 @@ namespace
hooks.clear();
}
bool HasMenuCommand(HMENU menu, UINT commandId) noexcept
{
return menu && GetMenuState(menu, commandId, MF_BYCOMMAND) != static_cast<UINT>(-1);
}
bool IsAlwaysOnTopMenuCommand(HMENU menu) noexcept
{
if (!HasMenuCommand(menu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND))
{
return false;
}
MENUITEMINFOW menuItemInfo{};
menuItemInfo.cbSize = sizeof(menuItemInfo);
menuItemInfo.fMask = MIIM_DATA;
return GetMenuItemInfoW(menu,
NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND,
FALSE,
&menuItemInfo) &&
menuItemInfo.dwItemData == NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND_OWNER_TAG;
}
}
bool isExcluded(HWND window)
@@ -47,7 +71,8 @@ bool isExcluded(HWND window)
auto processPath = get_process_path(window);
CharUpperBuffW(processPath.data(), static_cast<DWORD>(processPath.length()));
return check_excluded_app(window, processPath, AlwaysOnTopSettings::settings().excludedApps);
const auto settings = AlwaysOnTopSettings::settings();
return check_excluded_app(window, processPath, settings->excludedApps);
}
AlwaysOnTop::AlwaysOnTop(bool useLLKH, DWORD mainThreadId) :
@@ -131,7 +156,8 @@ void AlwaysOnTop::SettingsUpdate(SettingId id)
break;
case SettingId::FrameEnabled:
{
if (AlwaysOnTopSettings::settings().enableFrame)
const auto settings = AlwaysOnTopSettings::settings();
if (settings->enableFrame)
{
for (auto& iter : m_topmostWindows)
{
@@ -170,7 +196,8 @@ void AlwaysOnTop::SettingsUpdate(SettingId id)
break;
case SettingId::ShowInSystemMenu:
{
UpdateSystemMenuEventHooks(AlwaysOnTopSettings::settings().showInSystemMenu);
const auto settings = AlwaysOnTopSettings::settings();
UpdateSystemMenuEventHooks(settings->showInSystemMenu);
m_lastSystemMenuWindow = nullptr;
UpdateSystemMenuItem(GetForegroundWindow());
}
@@ -212,7 +239,7 @@ LRESULT AlwaysOnTop::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lp
void AlwaysOnTop::ProcessCommand(HWND window)
{
bool gameMode = detect_game_mode();
if (AlwaysOnTopSettings::settings().blockInGameMode && gameMode)
if (AlwaysOnTopSettings::settings()->blockInGameMode && gameMode)
{
return;
}
@@ -252,7 +279,7 @@ void AlwaysOnTop::ProcessCommand(HWND window)
}
}
if (AlwaysOnTopSettings::settings().enableSound)
if (AlwaysOnTopSettings::settings()->enableSound)
{
m_sound.Play(soundType);
}
@@ -299,7 +326,7 @@ void AlwaysOnTop::StartTrackingTopmostWindows()
bool AlwaysOnTop::AssignBorder(HWND window)
{
if (m_virtualDesktopUtils.IsWindowOnCurrentDesktop(window) && AlwaysOnTopSettings::settings().enableFrame)
if (m_virtualDesktopUtils.IsWindowOnCurrentDesktop(window) && AlwaysOnTopSettings::settings()->enableFrame)
{
auto border = WindowBorder::Create(window, m_hinstance);
if (border)
@@ -328,11 +355,13 @@ void AlwaysOnTop::RegisterHotkey() const
UnregisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity));
UnregisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity));
const auto settings = AlwaysOnTopSettings::settings();
// Register pin hotkey
RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), AlwaysOnTopSettings::settings().hotkey.get_modifiers(), AlwaysOnTopSettings::settings().hotkey.get_code());
RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), settings->hotkey.get_modifiers(), settings->hotkey.get_code());
// Register transparency hotkeys using the same modifiers as the pin hotkey
UINT modifiers = AlwaysOnTopSettings::settings().hotkey.get_modifiers();
UINT modifiers = settings->hotkey.get_modifiers();
RegisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity), modifiers, VK_OEM_PLUS);
RegisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity), modifiers, VK_OEM_MINUS);
}
@@ -448,7 +477,7 @@ void AlwaysOnTop::SubscribeToEvents()
}
}
UpdateSystemMenuEventHooks(AlwaysOnTopSettings::settings().showInSystemMenu);
UpdateSystemMenuEventHooks(AlwaysOnTopSettings::settings()->showInSystemMenu);
}
void AlwaysOnTop::UpdateSystemMenuEventHooks(bool enable)
@@ -501,9 +530,10 @@ void AlwaysOnTop::UpdateSystemMenuItem(HWND window) const noexcept
return;
}
if (!AlwaysOnTopSettings::settings().showInSystemMenu)
const auto settings = AlwaysOnTopSettings::settings();
if (!settings->showInSystemMenu)
{
if (GetMenuState(systemMenu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND, MF_BYCOMMAND) != static_cast<UINT>(-1))
if (IsAlwaysOnTopMenuCommand(systemMenu))
{
RemoveMenu(systemMenu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND, MF_BYCOMMAND);
}
@@ -513,20 +543,26 @@ void AlwaysOnTop::UpdateSystemMenuItem(HWND window) const noexcept
auto text = GET_RESOURCE_STRING(IDS_SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP);
MENUITEMINFOW menuItemInfo{};
menuItemInfo.cbSize = sizeof(menuItemInfo);
menuItemInfo.fMask = MIIM_ID | MIIM_STATE | MIIM_STRING;
menuItemInfo.fMask = MIIM_ID | MIIM_STATE | MIIM_STRING | MIIM_DATA;
menuItemInfo.wID = NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND;
menuItemInfo.fState = IsPinned(window) ? MFS_CHECKED : MFS_UNCHECKED;
menuItemInfo.dwTypeData = text.data();
menuItemInfo.dwItemData = NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND_OWNER_TAG;
if (GetMenuState(systemMenu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND, MF_BYCOMMAND) == static_cast<UINT>(-1))
if (!HasMenuCommand(systemMenu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND))
{
InsertMenuItemW(systemMenu, SC_CLOSE, FALSE, &menuItemInfo);
}
else
else if (IsAlwaysOnTopMenuCommand(systemMenu))
{
menuItemInfo.fMask = MIIM_STATE | MIIM_STRING;
SetMenuItemInfoW(systemMenu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND, FALSE, &menuItemInfo);
}
else
{
Logger::warn(L"Skipping Always On Top system menu command registration because ID 0x{:X} is already in use by another item.",
NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND);
}
}
void AlwaysOnTop::UnpinAll()
@@ -614,7 +650,7 @@ void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
{
if (data->idObject == OBJID_SYSMENU && data->hwnd)
{
m_lastSystemMenuWindow = AlwaysOnTopSettings::settings().showInSystemMenu ? data->hwnd : nullptr;
m_lastSystemMenuWindow = AlwaysOnTopSettings::settings()->showInSystemMenu ? data->hwnd : nullptr;
UpdateSystemMenuItem(data->hwnd);
}
}
@@ -629,7 +665,7 @@ void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
return;
case EVENT_OBJECT_INVOKED:
{
if (!AlwaysOnTopSettings::settings().showInSystemMenu)
if (!AlwaysOnTopSettings::settings()->showInSystemMenu)
{
return;
}
@@ -652,8 +688,7 @@ void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
}
const auto systemMenu = GetSystemMenu(window, false);
return systemMenu &&
GetMenuState(systemMenu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND, MF_BYCOMMAND) != static_cast<UINT>(-1);
return systemMenu && IsAlwaysOnTopMenuCommand(systemMenu);
};
HWND commandWindow = nullptr;
@@ -681,7 +716,7 @@ void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
break;
}
if (!AlwaysOnTopSettings::settings().enableFrame || !data->hwnd)
if (!AlwaysOnTopSettings::settings()->enableFrame || !data->hwnd)
{
return;
}
@@ -850,7 +885,7 @@ void AlwaysOnTop::StepWindowTransparency(HWND window, int delta)
{
ApplyWindowAlpha(targetWindow, newTransparency);
if (AlwaysOnTopSettings::settings().enableSound)
if (AlwaysOnTopSettings::settings()->enableSound)
{
m_sound.Play(delta > 0 ? Sound::Type::IncreaseOpacity : Sound::Type::DecreaseOpacity);
}

View File

@@ -44,12 +44,14 @@ inline COLORREF HexToRGB(std::wstring_view hex, const COLORREF fallbackColor = R
}
}
AlwaysOnTopSettings::AlwaysOnTopSettings()
AlwaysOnTopSettings::AlwaysOnTopSettings() :
m_settings(std::make_shared<Settings>())
{
m_uiSettings.ColorValuesChanged([&](winrt::Windows::UI::ViewManagement::UISettings const& settings,
winrt::Windows::Foundation::IInspectable const& args)
{
if (m_settings.frameAccentColor)
const auto currentSettings = AlwaysOnTopSettings::settings();
if (currentSettings->frameAccentColor)
{
NotifyObservers(SettingId::FrameAccentColor);
}
@@ -95,94 +97,97 @@ void AlwaysOnTopSettings::LoadSettings()
try
{
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(NonLocalizable::ModuleKey);
const auto currentSettings = AlwaysOnTopSettings::settings();
auto updatedSettings = std::make_shared<Settings>(*currentSettings);
std::vector<SettingId> changedSettings;
if (const auto jsonVal = values.get_json(NonLocalizable::HotkeyID))
{
auto val = PowerToysSettings::HotkeyObject::from_json(*jsonVal);
if (m_settings.hotkey.get_modifiers() != val.get_modifiers() || m_settings.hotkey.get_key() != val.get_key() || m_settings.hotkey.get_code() != val.get_code())
if (updatedSettings->hotkey.get_modifiers() != val.get_modifiers() || updatedSettings->hotkey.get_key() != val.get_key() || updatedSettings->hotkey.get_code() != val.get_code())
{
m_settings.hotkey = val;
NotifyObservers(SettingId::Hotkey);
updatedSettings->hotkey = val;
changedSettings.push_back(SettingId::Hotkey);
}
}
if (const auto jsonVal = values.get_bool_value(NonLocalizable::SoundEnabledID))
{
auto val = *jsonVal;
if (m_settings.enableSound != val)
if (updatedSettings->enableSound != val)
{
m_settings.enableSound = val;
NotifyObservers(SettingId::SoundEnabled);
updatedSettings->enableSound = val;
changedSettings.push_back(SettingId::SoundEnabled);
}
}
if (const auto jsonVal = values.get_bool_value(NonLocalizable::ShowInSystemMenuID))
{
auto val = *jsonVal;
if (m_settings.showInSystemMenu != val)
if (updatedSettings->showInSystemMenu != val)
{
m_settings.showInSystemMenu = val;
NotifyObservers(SettingId::ShowInSystemMenu);
updatedSettings->showInSystemMenu = val;
changedSettings.push_back(SettingId::ShowInSystemMenu);
}
}
if (const auto jsonVal = values.get_int_value(NonLocalizable::FrameThicknessID))
{
auto val = *jsonVal;
if (m_settings.frameThickness != val)
if (updatedSettings->frameThickness != val)
{
m_settings.frameThickness = val;
NotifyObservers(SettingId::FrameThickness);
updatedSettings->frameThickness = val;
changedSettings.push_back(SettingId::FrameThickness);
}
}
if (const auto jsonVal = values.get_string_value(NonLocalizable::FrameColorID))
{
auto val = HexToRGB(*jsonVal);
if (m_settings.frameColor != val)
if (updatedSettings->frameColor != val)
{
m_settings.frameColor = val;
NotifyObservers(SettingId::FrameColor);
updatedSettings->frameColor = val;
changedSettings.push_back(SettingId::FrameColor);
}
}
if (const auto jsonVal = values.get_int_value(NonLocalizable::FrameOpacityID))
{
auto val = *jsonVal;
if (m_settings.frameOpacity != val)
if (updatedSettings->frameOpacity != val)
{
m_settings.frameOpacity = val;
NotifyObservers(SettingId::FrameOpacity);
updatedSettings->frameOpacity = val;
changedSettings.push_back(SettingId::FrameOpacity);
}
}
if (const auto jsonVal = values.get_bool_value(NonLocalizable::FrameEnabledID))
{
auto val = *jsonVal;
if (m_settings.enableFrame != val)
if (updatedSettings->enableFrame != val)
{
m_settings.enableFrame = val;
NotifyObservers(SettingId::FrameEnabled);
updatedSettings->enableFrame = val;
changedSettings.push_back(SettingId::FrameEnabled);
}
}
if (const auto jsonVal = values.get_bool_value(NonLocalizable::BlockInGameModeID))
{
auto val = *jsonVal;
if (m_settings.blockInGameMode != val)
if (updatedSettings->blockInGameMode != val)
{
m_settings.blockInGameMode = val;
NotifyObservers(SettingId::BlockInGameMode);
updatedSettings->blockInGameMode = val;
changedSettings.push_back(SettingId::BlockInGameMode);
}
}
if (const auto jsonVal = values.get_bool_value(NonLocalizable::RoundCornersEnabledID))
{
auto val = *jsonVal;
if (m_settings.roundCornersEnabled != val)
if (updatedSettings->roundCornersEnabled != val)
{
m_settings.roundCornersEnabled = val;
NotifyObservers(SettingId::RoundCornersEnabled);
updatedSettings->roundCornersEnabled = val;
changedSettings.push_back(SettingId::RoundCornersEnabled);
}
}
@@ -203,20 +208,29 @@ void AlwaysOnTopSettings::LoadSettings()
view = left_trim<wchar_t>(trim<wchar_t>(view));
}
if (m_settings.excludedApps != excludedApps)
if (updatedSettings->excludedApps != excludedApps)
{
m_settings.excludedApps = excludedApps;
NotifyObservers(SettingId::ExcludeApps);
updatedSettings->excludedApps = excludedApps;
changedSettings.push_back(SettingId::ExcludeApps);
}
}
if (const auto jsonVal = values.get_bool_value(NonLocalizable::FrameAccentColor))
{
auto val = *jsonVal;
if (m_settings.frameAccentColor != val)
if (updatedSettings->frameAccentColor != val)
{
m_settings.frameAccentColor = val;
NotifyObservers(SettingId::FrameAccentColor);
updatedSettings->frameAccentColor = val;
changedSettings.push_back(SettingId::FrameAccentColor);
}
}
if (!changedSettings.empty())
{
m_settings.store(std::shared_ptr<const Settings>(updatedSettings), std::memory_order_release);
for (const auto changedSetting : changedSettings)
{
NotifyObservers(changedSetting);
}
}
}

View File

@@ -1,6 +1,9 @@
#pragma once
#include <atomic>
#include <memory>
#include <unordered_set>
#include <vector>
#include <common/SettingsAPI/FileWatcher.h>
#include <common/SettingsAPI/settings_objects.h>
@@ -34,9 +37,9 @@ class AlwaysOnTopSettings
{
public:
static AlwaysOnTopSettings& instance();
static inline const Settings& settings()
static inline std::shared_ptr<const Settings> settings()
{
return instance().m_settings;
return instance().m_settings.load(std::memory_order_acquire);
}
void InitFileWatcher();
@@ -52,7 +55,7 @@ private:
~AlwaysOnTopSettings() = default;
winrt::Windows::UI::ViewManagement::UISettings m_uiSettings;
Settings m_settings;
std::atomic<std::shared_ptr<const Settings>> m_settings;
std::unique_ptr<FileWatcher> m_settingsFileWatcher;
std::unordered_set<SettingsObserver*> m_observers;

View File

@@ -23,7 +23,7 @@ std::optional<RECT> GetFrameRect(HWND window)
return std::nullopt;
}
int border = AlwaysOnTopSettings::settings().frameThickness;
int border = AlwaysOnTopSettings::settings()->frameThickness;
rect.top -= border;
rect.left -= border;
rect.right += border;
@@ -194,8 +194,9 @@ void WindowBorder::UpdateBorderProperties() const
RECT frameRect{ 0, 0, windowRect.right - windowRect.left, windowRect.bottom - windowRect.top };
const auto settings = AlwaysOnTopSettings::settings();
COLORREF color;
if (AlwaysOnTopSettings::settings().frameAccentColor)
if (settings->frameAccentColor)
{
winrt::Windows::UI::ViewManagement::UISettings settings;
auto accentValue = settings.GetColorValue(winrt::Windows::UI::ViewManagement::UIColorType::Accent);
@@ -203,14 +204,14 @@ void WindowBorder::UpdateBorderProperties() const
}
else
{
color = AlwaysOnTopSettings::settings().frameColor;
color = settings->frameColor;
}
float opacity = AlwaysOnTopSettings::settings().frameOpacity / 100.0f;
float opacity = settings->frameOpacity / 100.0f;
float scalingFactor = ScalingUtils::ScalingFactor(m_trackingWindow);
float thickness = AlwaysOnTopSettings::settings().frameThickness * scalingFactor;
float thickness = settings->frameThickness * scalingFactor;
float cornerRadius = 0.0;
if (AlwaysOnTopSettings::settings().roundCornersEnabled)
if (settings->roundCornersEnabled)
{
cornerRadius = WindowCornerUtils::CornersRadius(m_trackingWindow) * scalingFactor;
}
@@ -268,7 +269,7 @@ LRESULT WindowBorder::WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexce
void WindowBorder::SettingsUpdate(SettingId id)
{
if (!AlwaysOnTopSettings::settings().enableFrame)
if (!AlwaysOnTopSettings::settings()->enableFrame)
{
return;
}

View File

@@ -3,7 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.5.250829002" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />

View File

@@ -0,0 +1,163 @@
// 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.Core.Common.Helpers;
public sealed partial class ThrottledDebouncedAction : IDisposable
{
private static readonly TimeSpan DefaultInterval = TimeSpan.FromMilliseconds(150);
private readonly Lock _lock = new();
private readonly Action _action;
private readonly TimeSpan _defaultInterval;
private readonly bool _runImmediately;
private CancellationTokenSource? _cts;
private bool _isRunning;
private bool _isPending;
private TimeSpan _pendingInterval;
public ThrottledDebouncedAction(Action action)
: this(action, DefaultInterval)
{
}
public ThrottledDebouncedAction(Action action, TimeSpan interval, bool runImmediately = false)
{
ArgumentNullException.ThrowIfNull(action);
ArgumentOutOfRangeException.ThrowIfLessThan(interval, TimeSpan.Zero);
_action = action;
_defaultInterval = interval;
_runImmediately = runImmediately;
}
public void Dispose()
{
Cancel();
}
public void Invoke() => Invoke(null);
public void Invoke(TimeSpan? interval)
{
var effectiveInterval = interval ?? _defaultInterval;
ArgumentOutOfRangeException.ThrowIfLessThan(effectiveInterval, TimeSpan.Zero);
if (effectiveInterval == TimeSpan.Zero)
{
Cancel();
_action();
return;
}
if (!_runImmediately)
{
// Trailing-edge debounce: each call resets the delay with the new interval.
CancellationTokenSource? oldCts;
CancellationToken token;
lock (_lock)
{
oldCts = _cts;
_cts = new CancellationTokenSource();
token = _cts.Token;
}
oldCts?.Cancel();
oldCts?.Dispose();
_ = Task.Run(
async () =>
{
try
{
await Task.Delay(effectiveInterval, token).ConfigureAwait(false);
if (token.IsCancellationRequested)
{
return;
}
_action();
}
catch (OperationCanceledException)
{
// expected during reschedules/dispose
}
},
CancellationToken.None);
}
else
{
// Leading + Trailing throttle/debounce
lock (_lock)
{
if (_isRunning)
{
_isPending = true;
_pendingInterval = effectiveInterval;
return;
}
_isRunning = true;
}
_action();
_ = Task.Run(async () =>
{
while (true)
{
TimeSpan delayInterval;
lock (_lock)
{
// Snapshot the interval to use for this cooldown.
// If no pending call yet, use the interval from the
// leading invocation; otherwise use the most recent
// pending interval (which may be updated by new calls
// arriving during the delay).
delayInterval = _isPending ? _pendingInterval : effectiveInterval;
}
await Task.Delay(delayInterval).ConfigureAwait(false);
bool shouldRun;
lock (_lock)
{
if (!_isPending)
{
_isRunning = false;
return;
}
_isPending = false;
shouldRun = true;
}
if (shouldRun)
{
_action();
}
}
});
}
}
public void InvokeImmediately() => Invoke(TimeSpan.Zero);
public void Cancel()
{
CancellationTokenSource? toCancel;
lock (_lock)
{
toCancel = _cts;
_cts = null;
_isPending = false;
_isRunning = false;
}
toCancel?.Cancel();
toCancel?.Dispose();
}
}

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Common.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {

View File

@@ -5,20 +5,23 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class AliasManager : ObservableObject
{
private readonly TopLevelCommandManager _topLevelCommandManager;
private readonly ISettingsService _settingsService;
// REMEMBER, CommandAlias.SearchPrefix is what we use as keys
private readonly Dictionary<string, CommandAlias> _aliases;
public AliasManager(TopLevelCommandManager tlcManager, SettingsModel settings)
public AliasManager(TopLevelCommandManager tlcManager, ISettingsService settingsService)
{
_topLevelCommandManager = tlcManager;
_aliases = settings.Aliases;
_settingsService = settingsService;
_aliases = _settingsService.Settings.Aliases;
if (_aliases.Count == 0)
{

View File

@@ -1,26 +1,14 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class AppStateModel : ObservableObject
{
[JsonIgnore]
public static readonly string FilePath;
public event TypedEventHandler<AppStateModel, object?>? StateChanged;
///////////////////////////////////////////////////////////////////////////
// STATE HERE
// Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)!
@@ -31,141 +19,4 @@ public partial class AppStateModel : ObservableObject
// END SETTINGS
///////////////////////////////////////////////////////////////////////////
static AppStateModel()
{
FilePath = StateJsonPath();
}
public static AppStateModel LoadState()
{
if (string.IsNullOrEmpty(FilePath))
{
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(LoadState)}");
}
if (!File.Exists(FilePath))
{
Debug.WriteLine("The provided settings file does not exist");
return new();
}
try
{
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
return loaded ?? new();
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
return new();
}
public static void SaveState(AppStateModel model)
{
if (string.IsNullOrEmpty(FilePath))
{
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(SaveState)}");
}
try
{
// Serialize the main dictionary to JSON and save it to the file
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel!);
// validate JSON
if (JsonNode.Parse(settingsJson) is not JsonObject newSettings)
{
Logger.LogError("Failed to parse app state as a JsonObject.");
return;
}
// read previous settings
if (!TryReadSavedState(out var savedSettings))
{
savedSettings = new JsonObject();
}
// merge new settings into old ones
foreach (var item in newSettings)
{
savedSettings[item.Key] = item.Value?.DeepClone();
}
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel!.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
// have a file change watcher on the settings file, and
// reload the settings then
model.StateChanged?.Invoke(model, null);
}
catch (Exception ex)
{
Logger.LogError($"Failed to save application state to {FilePath}:", ex);
}
}
private static bool TryReadSavedState([NotNullWhen(true)] out JsonObject? savedSettings)
{
savedSettings = null;
// read existing content from the file
string oldContent;
try
{
if (File.Exists(FilePath))
{
oldContent = File.ReadAllText(FilePath);
}
else
{
// file doesn't exist (might not have been created yet), so consider this a success
// and return empty settings
savedSettings = new JsonObject();
return true;
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to read app state file {FilePath}:\n{ex}");
return false;
}
// detect empty file, just for sake of logging
if (string.IsNullOrWhiteSpace(oldContent))
{
Logger.LogInfo($"App state file is empty: {FilePath}");
return false;
}
// is it valid JSON?
try
{
savedSettings = JsonNode.Parse(oldContent) as JsonObject;
return savedSettings != null;
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to parse app state from {FilePath}:\n{ex}");
return false;
}
}
internal static string StateJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the settings is just next to the exe
return Path.Combine(directory, "state.json");
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@@ -87,7 +87,8 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
Color.FromArgb(255, 126, 115, 95), // #7e735f
];
private readonly SettingsModel _settings;
private readonly ISettingsService _settingsService;
private readonly UISettings _uiSettings;
private readonly IThemeService _themeService;
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
@@ -100,18 +101,18 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int ThemeIndex
{
get => (int)_settings.Theme;
get => (int)_settingsService.Settings.Theme;
set => Theme = (UserTheme)value;
}
public UserTheme Theme
{
get => _settings.Theme;
get => _settingsService.Settings.Theme;
set
{
if (_settings.Theme != value)
if (_settingsService.Settings.Theme != value)
{
_settings.Theme = value;
_settingsService.Settings.Theme = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ThemeIndex));
Save();
@@ -121,12 +122,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public ColorizationMode ColorizationMode
{
get => _settings.ColorizationMode;
get => _settingsService.Settings.ColorizationMode;
set
{
if (_settings.ColorizationMode != value)
if (_settingsService.Settings.ColorizationMode != value)
{
_settings.ColorizationMode = value;
_settingsService.Settings.ColorizationMode = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorizationModeIndex));
OnPropertyChanged(nameof(IsCustomTintVisible));
@@ -152,18 +153,18 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int ColorizationModeIndex
{
get => (int)_settings.ColorizationMode;
get => (int)_settingsService.Settings.ColorizationMode;
set => ColorizationMode = (ColorizationMode)value;
}
public Color ThemeColor
{
get => _settings.CustomThemeColor;
get => _settingsService.Settings.CustomThemeColor;
set
{
if (_settings.CustomThemeColor != value)
if (_settingsService.Settings.CustomThemeColor != value)
{
_settings.CustomThemeColor = value;
_settingsService.Settings.CustomThemeColor = value;
OnPropertyChanged();
@@ -179,10 +180,10 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int ColorIntensity
{
get => _settings.CustomThemeColorIntensity;
get => _settingsService.Settings.CustomThemeColorIntensity;
set
{
_settings.CustomThemeColorIntensity = value;
_settingsService.Settings.CustomThemeColorIntensity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveTintIntensity));
Save();
@@ -191,10 +192,10 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackgroundImageTintIntensity
{
get => _settings.BackgroundImageTintIntensity;
get => _settingsService.Settings.BackgroundImageTintIntensity;
set
{
_settings.BackgroundImageTintIntensity = value;
_settingsService.Settings.BackgroundImageTintIntensity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveTintIntensity));
Save();
@@ -203,12 +204,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public string BackgroundImagePath
{
get => _settings.BackgroundImagePath ?? string.Empty;
get => _settingsService.Settings.BackgroundImagePath ?? string.Empty;
set
{
if (_settings.BackgroundImagePath != value)
if (_settingsService.Settings.BackgroundImagePath != value)
{
_settings.BackgroundImagePath = value;
_settingsService.Settings.BackgroundImagePath = value;
OnPropertyChanged();
if (BackgroundImageOpacity == 0)
@@ -223,12 +224,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackgroundImageOpacity
{
get => _settings.BackgroundImageOpacity;
get => _settingsService.Settings.BackgroundImageOpacity;
set
{
if (_settings.BackgroundImageOpacity != value)
if (_settingsService.Settings.BackgroundImageOpacity != value)
{
_settings.BackgroundImageOpacity = value;
_settingsService.Settings.BackgroundImageOpacity = value;
OnPropertyChanged();
Save();
}
@@ -237,12 +238,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackgroundImageBrightness
{
get => _settings.BackgroundImageBrightness;
get => _settingsService.Settings.BackgroundImageBrightness;
set
{
if (_settings.BackgroundImageBrightness != value)
if (_settingsService.Settings.BackgroundImageBrightness != value)
{
_settings.BackgroundImageBrightness = value;
_settingsService.Settings.BackgroundImageBrightness = value;
OnPropertyChanged();
Save();
}
@@ -251,12 +252,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackgroundImageBlurAmount
{
get => _settings.BackgroundImageBlurAmount;
get => _settingsService.Settings.BackgroundImageBlurAmount;
set
{
if (_settings.BackgroundImageBlurAmount != value)
if (_settingsService.Settings.BackgroundImageBlurAmount != value)
{
_settings.BackgroundImageBlurAmount = value;
_settingsService.Settings.BackgroundImageBlurAmount = value;
OnPropertyChanged();
Save();
}
@@ -265,12 +266,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public BackgroundImageFit BackgroundImageFit
{
get => _settings.BackgroundImageFit;
get => _settingsService.Settings.BackgroundImageFit;
set
{
if (_settings.BackgroundImageFit != value)
if (_settingsService.Settings.BackgroundImageFit != value)
{
_settings.BackgroundImageFit = value;
_settingsService.Settings.BackgroundImageFit = value;
OnPropertyChanged();
OnPropertyChanged(nameof(BackgroundImageFitIndex));
Save();
@@ -299,12 +300,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackdropOpacity
{
get => _settings.BackdropOpacity;
get => _settingsService.Settings.BackdropOpacity;
set
{
if (_settings.BackdropOpacity != value)
if (_settingsService.Settings.BackdropOpacity != value)
{
_settings.BackdropOpacity = value;
_settingsService.Settings.BackdropOpacity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveBackdropStyle));
OnPropertyChanged(nameof(EffectiveImageOpacity));
@@ -315,13 +316,13 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackdropStyleIndex
{
get => (int)_settings.BackdropStyle;
get => (int)_settingsService.Settings.BackdropStyle;
set
{
var newStyle = (BackdropStyle)value;
if (_settings.BackdropStyle != newStyle)
if (_settingsService.Settings.BackdropStyle != newStyle)
{
_settings.BackdropStyle = newStyle;
_settingsService.Settings.BackdropStyle = newStyle;
OnPropertyChanged();
OnPropertyChanged(nameof(IsBackdropOpacityVisible));
@@ -343,25 +344,25 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
/// Gets whether the backdrop opacity slider should be visible.
/// </summary>
public bool IsBackdropOpacityVisible =>
BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets whether the backdrop description (for styles without options) should be visible.
/// </summary>
public bool IsMicaBackdropDescriptionVisible =>
!BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets whether background/colorization settings are available.
/// </summary>
public bool IsBackgroundSettingsEnabled =>
BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
/// <summary>
/// Gets whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
/// </summary>
public bool IsBackgroundNotAvailableVisible =>
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
public BackdropStyle? EffectiveBackdropStyle
{
@@ -370,9 +371,9 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
// Return style when transparency/blur is visible (not fully opaque Acrylic)
// - Clear/Mica/MicaAlt/AcrylicThin always show their effect
// - Acrylic shows effect only when opacity < 100
if (_settings.BackdropStyle != BackdropStyle.Acrylic || _settings.BackdropOpacity < 100)
if (_settingsService.Settings.BackdropStyle != BackdropStyle.Acrylic || _settingsService.Settings.BackdropOpacity < 100)
{
return _settings.BackdropStyle;
return _settingsService.Settings.BackdropStyle;
}
return null;
@@ -381,39 +382,39 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public double EffectiveImageOpacity =>
EffectiveBackdropStyle is not null
? (BackgroundImageOpacity / 100f) * Math.Sqrt(_settings.BackdropOpacity / 100.0)
? (BackgroundImageOpacity / 100f) * Math.Sqrt(_settingsService.Settings.BackdropOpacity / 100.0)
: (BackgroundImageOpacity / 100f);
[ObservableProperty]
public partial bool IsColorizationDetailsExpanded { get; set; }
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsCustomTintVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsColorIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
public bool IsColorIntensityVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
public bool IsImageTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.Image;
public bool IsImageTintIntensityVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
/// <summary>
/// Gets the effective tint intensity for the preview, based on the current colorization mode.
/// </summary>
public int EffectiveTintIntensity => _settings.ColorizationMode is ColorizationMode.Image
? _settings.BackgroundImageTintIntensity
: _settings.CustomThemeColorIntensity;
public int EffectiveTintIntensity => _settingsService.Settings.ColorizationMode is ColorizationMode.Image
? _settingsService.Settings.BackgroundImageTintIntensity
: _settingsService.Settings.CustomThemeColorIntensity;
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
public bool IsBackgroundControlsVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None;
public bool IsNoBackgroundVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.None;
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
public bool IsAccentColorControlsVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
public bool IsResetButtonVisible => _settings.ColorizationMode is ColorizationMode.Image;
public bool IsResetButtonVisible => _settingsService.Settings.ColorizationMode is ColorizationMode.Image;
public BackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
public Color EffectiveThemeColor =>
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization
? Colors.Transparent
: ColorizationMode switch
{
@@ -428,7 +429,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
public ImageSource? EffectiveBackgroundImageSource =>
!BackdropStyles.Get(_settings.BackdropStyle).SupportsBackgroundImage
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsBackgroundImage
? null
: ColorizationMode is ColorizationMode.Image
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
@@ -436,11 +437,11 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
: null;
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
public AppearanceSettingsViewModel(IThemeService themeService, ISettingsService settingsService)
{
_themeService = themeService;
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
_settings = settings;
_settingsService = settingsService;
_uiSettings = new UISettings();
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
@@ -448,7 +449,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
Reapply();
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
IsColorizationDetailsExpanded = _settingsService.Settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
}
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
@@ -469,7 +470,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
private void Save()
{
SettingsModel.SaveSettings(_settings);
_settingsService.Save();
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
}

View File

@@ -15,7 +15,7 @@ internal static class BatchUpdateManager
// 30 ms chosen empirically to balance responsiveness and batching:
// - Keeps perceived latency low (< ~50 ms) for user-visible updates.
// - Still allows multiple COM/background events to be coalesced into a single batch.
private static readonly TimeSpan BatchDelay = TimeSpan.FromMilliseconds(30);
private static readonly TimeSpan BatchDelay = TimeSpan.FromMilliseconds(40);
private static readonly ConcurrentQueue<IBatchUpdateTarget> DirtyQueue = [];
private static readonly Timer Timer = new(static _ => Flush(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);

View File

@@ -2,31 +2,54 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
using DispatcherQueueTimer = Microsoft.UI.Dispatching.DispatcherQueueTimer;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandBarViewModel : ObservableObject,
public sealed partial class CommandBarViewModel : ObservableObject,
IRecipient<UpdateCommandBarMessage>
{
private readonly DispatcherQueueTimer _debounceTimer;
private volatile ICommandBarContext? _pendingSelectedItem;
public ICommandBarContext? SelectedItem
{
get => field;
get;
set
{
if (field != null)
// TODO: verify if we can safely return early
// if (ReferenceEquals(field, value))
// {
// return;
// }
if (field is not null)
{
field.PropertyChanged -= SelectedItemPropertyChanged;
}
field = value;
SetSelectedItem(value);
OnPropertyChanged(nameof(SelectedItem));
if (field is not null)
{
PrimaryCommand = field.PrimaryCommand;
field.PropertyChanged += SelectedItemPropertyChanged;
}
else
{
PrimaryCommand = null;
}
UpdateContextItems();
OnPropertyChanged();
}
}
@@ -34,6 +57,8 @@ public partial class CommandBarViewModel : ObservableObject,
[NotifyPropertyChangedFor(nameof(HasPrimaryCommand))]
public partial CommandItemViewModel? PrimaryCommand { get; set; }
// TODO: PrimaryCommand.ShouldBeVisible is not observed, if it changes the bar won't refresh;
// but at this moment CommandItemViewModel won't raise INPC for ShouldBeVisible anyway.
public bool HasPrimaryCommand => PrimaryCommand is not null && PrimaryCommand.ShouldBeVisible;
[ObservableProperty]
@@ -50,29 +75,31 @@ public partial class CommandBarViewModel : ObservableObject,
public CommandBarViewModel()
{
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
if (dispatcherQueue is null)
{
throw new InvalidOperationException("DispatcherQueue is not available for the current thread.");
}
_debounceTimer = dispatcherQueue.CreateTimer();
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
}
public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel;
private void SetSelectedItem(ICommandBarContext? value)
public void Receive(UpdateCommandBarMessage message)
{
if (value is not null)
{
PrimaryCommand = value.PrimaryCommand;
value.PropertyChanged += SelectedItemPropertyChanged;
}
else
{
if (SelectedItem is not null)
{
SelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
}
_pendingSelectedItem = message.ViewModel;
PrimaryCommand = null;
}
// immediate: false is intentional — the timer tick always fires on the
// dispatcher queue thread, which guarantees ApplyPendingSelectedItem
// runs on the UI thread even if Receive is called from a background
// thread. Using immediate: true would invoke the delegate synchronously
// on the calling thread, bypassing the dispatcher.
_debounceTimer.Debounce(ApplyPendingSelectedItem, TimeSpan.FromMilliseconds(50));
}
UpdateContextItems();
private void ApplyPendingSelectedItem()
{
SelectedItem = _pendingSelectedItem;
}
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
@@ -95,11 +122,9 @@ public partial class CommandBarViewModel : ObservableObject,
}
SecondaryCommand = SelectedItem.SecondaryCommand;
var moreCommands = SelectedItem.MoreCommands;
var hasMoreThanOneContextItem = SelectedItem.MoreCommands.Count() > 1;
var hasMoreThanOneCommand = SelectedItem.MoreCommands.OfType<CommandContextItemViewModel>().Any();
ShouldShowContextMenu = hasMoreThanOneContextItem && hasMoreThanOneCommand;
ShouldShowContextMenu = moreCommands.Count > 1 && SelectedItem.HasMoreCommands;
OnPropertyChanged(nameof(HasSecondaryCommand));
OnPropertyChanged(nameof(SecondaryCommand));

View File

@@ -21,6 +21,12 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
private readonly IContextMenuFactory? _contextMenuFactory;
private readonly Lock _moreCommandsLock = new();
private readonly List<IContextItemViewModel> _moreCommands = [];
private volatile CommandContextItemViewModel? _secondaryMoreCommand;
private volatile IContextItemViewModel[] _moreCommandsSnapshot = [];
private volatile IContextItemViewModel[] _allCommandsSnapshot = [];
private ExtensionObject<IExtendedAttributesProvider>? ExtendedAttributesProvider { get; set; }
private readonly ExtensionObject<ICommandItem> _commandItemModel = new(null);
@@ -63,33 +69,22 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public CommandViewModel Command { get; private set; }
public List<IContextItemViewModel> MoreCommands { get; private set; } = [];
// Reuse a cached read-only snapshot so repeated reads don't allocate.
public IReadOnlyList<IContextItemViewModel> MoreCommands => _moreCommandsSnapshot;
IEnumerable<IContextItemViewModel> IContextMenuContext.MoreCommands => MoreCommands;
IReadOnlyList<IContextItemViewModel> IContextMenuContext.MoreCommands => _moreCommandsSnapshot;
private List<CommandContextItemViewModel> ActualCommands => MoreCommands.OfType<CommandContextItemViewModel>().ToList();
protected Lock MoreCommandsLock => _moreCommandsLock;
public bool HasMoreCommands => ActualCommands.Count > 0;
protected List<IContextItemViewModel> UnsafeMoreCommands => _moreCommands;
public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
public bool HasMoreCommands => _secondaryMoreCommand is not null;
public string SecondaryCommandName => _secondaryMoreCommand?.Name ?? string.Empty;
public CommandItemViewModel? PrimaryCommand => this;
public CommandItemViewModel? SecondaryCommand
{
get
{
if (HasMoreCommands)
{
if (MoreCommands[0] is CommandContextItemViewModel command)
{
return command;
}
}
return null;
}
}
public CommandItemViewModel? SecondaryCommand => _secondaryMoreCommand;
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
@@ -101,18 +96,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public DataPackageView? DataPackage { get; private set; }
public List<IContextItemViewModel> AllCommands
{
get
{
List<IContextItemViewModel> l = _defaultCommandContextItemViewModel is null ?
new() :
[_defaultCommandContextItemViewModel];
l.AddRange(MoreCommands);
return l;
}
}
public IReadOnlyList<IContextItemViewModel> AllCommands => _allCommandsSnapshot;
private static readonly IconInfoViewModel _errorIcon;
@@ -246,6 +230,11 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
UpdateDefaultContextItemIcon();
}
lock (_moreCommandsLock)
{
RefreshMoreCommandStateUnsafe();
}
Initialized |= InitializedState.SelectionInitialized;
UpdateProperty(nameof(MoreCommands));
UpdateProperty(nameof(AllCommands));
@@ -265,7 +254,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Command = new(null, PageContext);
_itemTitle = "Error";
Subtitle = "Item failed to load";
MoreCommands = [];
ClearMoreCommands();
_icon = _errorIcon;
_titleCache.Invalidate();
_subtitleCache.Invalidate();
@@ -304,7 +293,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
Command = new(null, PageContext);
_itemTitle = "Error";
Subtitle = "Item failed to load";
MoreCommands = [];
ClearMoreCommands();
_icon = _errorIcon;
_titleCache.Invalidate();
_subtitleCache.Invalidate();
@@ -385,9 +374,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
case nameof(model.MoreCommands):
BuildAndInitMoreCommands();
UpdateProperty(nameof(SecondaryCommand));
UpdateProperty(nameof(SecondaryCommandName));
UpdateProperty(nameof(HasMoreCommands));
UpdateProperty(nameof(SecondaryCommand), nameof(SecondaryCommandName), nameof(HasMoreCommands), nameof(AllCommands));
break;
case nameof(DataPackage):
@@ -478,9 +465,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
var results = factory.UnsafeBuildAndInitMoreCommands(more, this);
List<IContextItemViewModel>? freedItems;
lock (MoreCommands)
lock (_moreCommandsLock)
{
ListHelpers.InPlaceUpdateList(MoreCommands, results, out freedItems);
ListHelpers.InPlaceUpdateList(_moreCommands, results, out freedItems);
RefreshMoreCommandStateUnsafe();
}
freedItems.OfType<CommandContextItemViewModel>()
@@ -516,20 +504,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
{
base.UnsafeCleanup();
lock (MoreCommands)
List<IContextItemViewModel> freedItems;
CommandContextItemViewModel? freedDefault;
lock (_moreCommandsLock)
{
MoreCommands.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(c => c.SafeCleanup());
MoreCommands.Clear();
freedItems = [.. _moreCommands];
_moreCommands.Clear();
// Null out here so the single RefreshMoreCommandStateUnsafe call
// produces an _allCommandsSnapshot that excludes the default command.
freedDefault = _defaultCommandContextItemViewModel;
_defaultCommandContextItemViewModel = null;
RefreshMoreCommandStateUnsafe();
}
// Cleanup outside lock to avoid holding it during RPC calls
freedItems.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(c => c.SafeCleanup());
freedDefault?.SafeCleanup();
// _listItemIcon.SafeCleanup();
_icon = new(null); // necessary?
_defaultCommandContextItemViewModel?.SafeCleanup();
_defaultCommandContextItemViewModel = null;
Command.PropertyChanged -= Command_PropertyChanged;
Command.SafeCleanup();
@@ -545,6 +543,40 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
base.SafeCleanup();
Initialized |= InitializedState.CleanedUp;
}
protected void RefreshMoreCommandStateUnsafe()
{
_moreCommandsSnapshot = [.. _moreCommands];
_secondaryMoreCommand = null;
foreach (var item in _moreCommands)
{
if (item is CommandContextItemViewModel command)
{
_secondaryMoreCommand = command;
break;
}
}
_allCommandsSnapshot = _defaultCommandContextItemViewModel is null ?
_moreCommandsSnapshot :
[_defaultCommandContextItemViewModel, .. _moreCommandsSnapshot];
}
private void ClearMoreCommands()
{
List<IContextItemViewModel> freedItems;
lock (_moreCommandsLock)
{
freedItems = [.. _moreCommands];
_moreCommands.Clear();
RefreshMoreCommandStateUnsafe();
}
freedItems.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(c => c.SafeCleanup());
}
}
[Flags]

View File

@@ -2,6 +2,7 @@
// 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.CmdPal.UI.ViewModels.MainPage;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -22,6 +23,7 @@ public class CommandPalettePageViewModelFactory
{
return page switch
{
MainListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsRootPage = !nested, IsMainPage = true },
IListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsRootPage = !nested },
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host, providerContext),
_ => null,

View File

@@ -139,7 +139,8 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
return;
}
var settings = serviceProvider.GetService<SettingsModel>()!;
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
var providerSettings = GetProviderSettings(settings);
IsActive = providerSettings.IsEnabled;
@@ -215,9 +216,7 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
}
catch (Exception e)
{
Logger.LogError("Failed to load commands from extension");
Logger.LogError($"Extension was {Extension!.PackageFamilyName}");
Logger.LogError(e.ToString());
Logger.LogError($"Failed to load commands from extension {Extension!.PackageFamilyName}", e);
if (!displayInfoInitialized)
{
@@ -251,16 +250,15 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
IServiceProvider serviceProvider,
ICommandProvider4? four)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var settings = serviceProvider.GetRequiredService<ISettingsService>().Settings;
var contextMenuFactory = serviceProvider.GetService<IContextMenuFactory>()!;
var state = serviceProvider.GetService<AppStateModel>()!;
var providerSettings = GetProviderSettings(settings);
var ourContext = GetProviderContext();
WeakReference<IPageContext> pageContext = new(this.TopLevelPageContext);
var make = (ICommandItem? i, TopLevelType t) =>
{
CommandItemViewModel commandItemViewModel = new(new(i), pageContext, contextMenuFactory: contextMenuFactory);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i, contextMenuFactory: contextMenuFactory);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ourContext, providerSettings, serviceProvider, i, contextMenuFactory: contextMenuFactory);
topLevelViewModel.InitializeProperties();
return topLevelViewModel;
@@ -409,7 +407,8 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
public void PinCommand(string commandId, IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
var providerSettings = GetProviderSettings(settings);
if (!providerSettings.PinnedCommandIds.Contains(commandId))
@@ -418,13 +417,14 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
SettingsModel.SaveSettings(settings, false);
settingsService.Save(hotReload: false);
}
}
public void UnpinCommand(string commandId, IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
var providerSettings = GetProviderSettings(settings);
if (providerSettings.PinnedCommandIds.Remove(commandId))
@@ -432,13 +432,14 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
SettingsModel.SaveSettings(settings, false);
settingsService.Save(hotReload: false);
}
}
public void PinDockBand(string commandId, IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
var bandSettings = new DockBandSettings
{
CommandId = commandId,
@@ -449,19 +450,20 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
SettingsModel.SaveSettings(settings, false);
settingsService.Save(hotReload: false);
}
public void UnpinDockBand(string commandId, IServiceProvider serviceProvider)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var settings = settingsService.Settings;
settings.DockSettings.StartBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
settings.DockSettings.CenterBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
settings.DockSettings.EndBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
SettingsModel.SaveSettings(settings, false);
settingsService.Save(hotReload: false);
}
public ICommandProviderContext GetProviderContext() => this;

View File

@@ -2,18 +2,23 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
/*
#define CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
*/
using System.Collections.Specialized;
using System.Diagnostics;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Text;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Ext.Apps;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.UI.ViewModels.Commands;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Properties;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -25,12 +30,21 @@ namespace Microsoft.CmdPal.UI.ViewModels.MainPage;
/// </summary>
public sealed partial class MainListPage : DynamicListPage,
IRecipient<ClearSearchMessage>,
IRecipient<UpdateFallbackItemsMessage>, IDisposable
IRecipient<UpdateFallbackItemsMessage>,
IDisposable
{
// Throttle for raising items changed events from external sources
private static readonly TimeSpan RaiseItemsChangedThrottle = TimeSpan.FromMilliseconds(100);
// Throttle for raising items changed events from user input - we want this to feel more responsive, so a shorter throttle.
private static readonly TimeSpan RaiseItemsChangedThrottleForUserInput = TimeSpan.FromMilliseconds(50);
private readonly FallbackUpdateManager _fallbackUpdateManager;
private readonly ThrottledDebouncedAction _refreshThrottledDebouncedAction;
private readonly TopLevelCommandManager _tlcManager;
private readonly AliasManager _aliasManager;
private readonly SettingsModel _settings;
private readonly AppStateModel _appStateModel;
private readonly ISettingsService _settingsService;
private readonly IAppStateService _appStateService;
private readonly ScoringFunction<IListItem> _scoringFunction;
private readonly ScoringFunction<IListItem> _fallbackScoringFunction;
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
@@ -39,6 +53,7 @@ public sealed partial class MainListPage : DynamicListPage,
// recognise them across successive GetItems() calls
private readonly Separator _resultsSeparator = new(Resources.results);
private readonly Separator _fallbacksSeparator = new(Resources.fallbacks);
private readonly Separator _commandsSeparator = new(Resources.home_sections_commands_title);
private RoScored<IListItem>[]? _filteredItems;
private RoScored<IListItem>[]? _filteredApps;
@@ -53,50 +68,91 @@ public sealed partial class MainListPage : DynamicListPage,
private int AppResultLimit => AllAppsCommandProvider.TopLevelResultLimit;
private InterlockedBoolean _fullRefreshRequested;
private InterlockedBoolean _refreshRunning;
private InterlockedBoolean _refreshRequested;
private CancellationTokenSource? _cancellationTokenSource;
#if CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
private DateTimeOffset _last = DateTimeOffset.UtcNow;
#endif
public MainListPage(
TopLevelCommandManager topLevelCommandManager,
SettingsModel settings,
AliasManager aliasManager,
AppStateModel appStateModel,
IFuzzyMatcherProvider fuzzyMatcherProvider)
IFuzzyMatcherProvider fuzzyMatcherProvider,
ISettingsService settingsService,
IAppStateService appStateService)
{
Id = "com.microsoft.cmdpal.home";
Title = Resources.builtin_home_name;
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;
_settings = settings;
_settingsService = settingsService;
_aliasManager = aliasManager;
_appStateModel = appStateModel;
_appStateService = appStateService;
_tlcManager = topLevelCommandManager;
_fuzzyMatcherProvider = fuzzyMatcherProvider;
_scoringFunction = (in query, item) => ScoreTopLevelItem(in query, item, _appStateModel.RecentCommands, _fuzzyMatcherProvider.Current);
_fallbackScoringFunction = (in _, item) => ScoreFallbackItem(item, _settings.FallbackRanks);
_scoringFunction = (in query, item) => ScoreTopLevelItem(in query, item, _appStateService.State.RecentCommands, _fuzzyMatcherProvider.Current);
_fallbackScoringFunction = (in _, item) => ScoreFallbackItem(item, _settingsService.Settings.FallbackRanks);
_tlcManager.PropertyChanged += TlcManager_PropertyChanged;
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
_refreshThrottledDebouncedAction = new ThrottledDebouncedAction(
() =>
{
try
{
#if CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
var delta = DateTimeOffset.UtcNow - _last;
_last = DateTimeOffset.UtcNow;
Logger.LogDebug($"UpdateFallbacks: RaiseItemsChanged, delta {delta}");
var sw = Stopwatch.StartNew();
#endif
if (_fullRefreshRequested.Clear())
{
// full refresh
RaiseItemsChanged();
}
else
{
// preserve selection
RaiseItemsChanged(ListViewModel.IncrementalRefresh);
}
#if CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
Logger.LogInfo($"UpdateFallbacks: RaiseItemsChanged took {sw.Elapsed}");
#endif
}
catch (Exception ex)
{
Logger.LogError("Unhandled exception in MainListPage refresh debounced action", ex);
}
},
RaiseItemsChangedThrottle);
_fallbackUpdateManager = new FallbackUpdateManager(() => RequestRefresh(fullRefresh: false));
// The all apps page will kick off a BG thread to start loading apps.
// We just want to know when it is done.
var allApps = AllAppsCommandProvider.Page;
allApps.PropChanged += (s, p) =>
{
if (p.PropertyName == nameof(allApps.IsLoading))
{
if (p.PropertyName == nameof(allApps.IsLoading))
{
IsLoading = ActuallyLoading();
}
};
IsLoading = ActuallyLoading();
}
};
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateFallbackItemsMessage>(this);
settings.SettingsChanged += SettingsChangedHandler;
HotReloadSettings(settings);
_settingsService.SettingsChanged += SettingsChangedHandler;
HotReloadSettings(_settingsService.Settings);
_includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId);
IsLoading = true;
@@ -119,10 +175,20 @@ public sealed partial class MainListPage : DynamicListPage,
}
else
{
RaiseItemsChanged();
RequestRefresh(fullRefresh: false);
}
}
private void RequestRefresh(bool fullRefresh, TimeSpan? interval = null)
{
if (fullRefresh)
{
_fullRefreshRequested.Set();
}
_refreshThrottledDebouncedAction.Invoke(interval);
}
private void ReapplySearchInBackground()
{
_refreshRequested.Set();
@@ -150,7 +216,7 @@ public sealed partial class MainListPage : DynamicListPage,
}
var currentSearchText = SearchText;
UpdateSearchText(currentSearchText, currentSearchText);
UpdateSearchTextCore(currentSearchText, currentSearchText, isUserInput: false);
}
while (_refreshRequested.Value);
}
@@ -196,7 +262,7 @@ public sealed partial class MainListPage : DynamicListPage,
// +1 for the separator
var result = new IListItem[eligibleCount + 1];
result[0] = _resultsSeparator;
result[0] = _commandsSeparator;
// Second pass: populate
var writeIndex = 1;
@@ -242,6 +308,11 @@ public sealed partial class MainListPage : DynamicListPage,
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
UpdateSearchTextCore(oldSearch, newSearch, isUserInput: true);
}
private void UpdateSearchTextCore(string oldSearch, string newSearch, bool isUserInput)
{
var stopwatch = Stopwatch.StartNew();
@@ -294,9 +365,9 @@ public sealed partial class MainListPage : DynamicListPage,
}
// prefilter fallbacks
var globalFallbacks = _settings.GetGlobalFallbacks();
var globalFallbacks = _settingsService.Settings.GetGlobalFallbacks();
var specialFallbacks = new List<TopLevelViewModel>(globalFallbacks.Length);
var commonFallbacks = new List<TopLevelViewModel>();
var commonFallbacks = new List<TopLevelViewModel>(commands.Count - globalFallbacks.Length);
foreach (var s in commands)
{
@@ -315,10 +386,7 @@ public sealed partial class MainListPage : DynamicListPage,
}
}
// start update of fallbacks; update special fallbacks separately,
// so they can finish faster
UpdateFallbacks(SearchText, specialFallbacks, token);
UpdateFallbacks(SearchText, commonFallbacks, token);
_fallbackUpdateManager.BeginUpdate(SearchText, [.. specialFallbacks, .. commonFallbacks], token);
if (token.IsCancellationRequested)
{
@@ -326,11 +394,13 @@ public sealed partial class MainListPage : DynamicListPage,
}
// Cleared out the filter text? easy. Reset _filteredItems, and bail out.
if (string.IsNullOrEmpty(newSearch))
if (string.IsNullOrWhiteSpace(newSearch))
{
_filteredItemsIncludesApps = _includeApps;
ClearResults();
RaiseItemsChanged(commands.Count);
var wasAlreadyEmpty = string.IsNullOrWhiteSpace(oldSearch);
RequestRefresh(fullRefresh: true, interval: wasAlreadyEmpty ? null : TimeSpan.Zero);
return;
}
@@ -410,7 +480,7 @@ public sealed partial class MainListPage : DynamicListPage,
// We need to remove pinned apps from allNewApps so they don't show twice.
// Pinned app command IDs are stored in ProviderSettings.PinnedCommandIds.
_settings.ProviderSettings.TryGetValue(AllAppsCommandProvider.WellKnownId, out var providerSettings);
_settingsService.Settings.ProviderSettings.TryGetValue(AllAppsCommandProvider.WellKnownId, out var providerSettings);
var pinnedCommandIds = providerSettings?.PinnedCommandIds;
if (pinnedCommandIds is not null && pinnedCommandIds.Count > 0)
@@ -465,49 +535,35 @@ public sealed partial class MainListPage : DynamicListPage,
}
}
#if CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
var filterDoneTimestamp = stopwatch.ElapsedMilliseconds;
Logger.LogDebug($"Filter with '{newSearch}' in {filterDoneTimestamp}ms");
#endif
if (isUserInput)
{
// Make sure that the throttle delay is consistent from the user's perspective, even if filtering
// takes a long time. If we always use the full throttle duration, then a slow filter could make the UI feel sluggish.
var adjustedInterval = RaiseItemsChangedThrottleForUserInput - stopwatch.Elapsed;
if (adjustedInterval < TimeSpan.Zero)
{
adjustedInterval = TimeSpan.Zero;
}
RaiseItemsChanged();
RequestRefresh(fullRefresh: true, adjustedInterval);
}
else
{
RequestRefresh(fullRefresh: true);
}
#if CMDPAL_FF_MAINPAGE_TIME_RAISE_ITEMS
var listPageUpdatedTimestamp = stopwatch.ElapsedMilliseconds;
Logger.LogDebug($"Render items with '{newSearch}' in {listPageUpdatedTimestamp}ms /d {listPageUpdatedTimestamp - filterDoneTimestamp}ms");
#endif
stopwatch.Stop();
}
}
private void UpdateFallbacks(string newSearch, IReadOnlyList<TopLevelViewModel> commands, CancellationToken token)
{
_ = Task.Run(
() =>
{
var needsToUpdate = false;
foreach (var command in commands)
{
if (token.IsCancellationRequested)
{
return;
}
var changedVisibility = command.SafeUpdateFallbackTextSynchronous(newSearch);
needsToUpdate = needsToUpdate || changedVisibility;
}
if (needsToUpdate)
{
if (token.IsCancellationRequested)
{
return;
}
RaiseItemsChanged();
}
},
token);
}
private bool ActuallyLoading()
{
var allApps = AllAppsCommandProvider.Page;
@@ -623,9 +679,9 @@ public sealed partial class MainListPage : DynamicListPage,
public void UpdateHistory(IListItem topLevelOrAppItem)
{
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
var history = _appStateModel.RecentCommands;
var history = _appStateService.State.RecentCommands;
history.AddHistoryItem(id);
AppStateModel.SaveState(_appStateModel);
_appStateService.Save();
}
private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
@@ -643,9 +699,12 @@ public sealed partial class MainListPage : DynamicListPage,
public void Receive(ClearSearchMessage message) => SearchText = string.Empty;
public void Receive(UpdateFallbackItemsMessage message) => RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
public void Receive(UpdateFallbackItemsMessage message)
{
RequestRefresh(fullRefresh: false);
}
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(sender);
private void SettingsChangedHandler(ISettingsService sender, SettingsModel args) => HotReloadSettings(args);
private void HotReloadSettings(SettingsModel settings) => ShowDetails = settings.ShowAppDetails;
@@ -653,13 +712,14 @@ public sealed partial class MainListPage : DynamicListPage,
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_fallbackUpdateManager.Dispose();
_tlcManager.PropertyChanged -= TlcManager_PropertyChanged;
_tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged;
if (_settings is not null)
if (_settingsService is not null)
{
_settings.SettingsChanged -= SettingsChangedHandler;
_settingsService.SettingsChanged -= SettingsChangedHandler;
}
WeakReferenceMessenger.Default.UnregisterAll(this);

View File

@@ -30,7 +30,8 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
{
"type": "TextBlock",
"text": {{FormatJsonString(Properties.Resources.builtin_create_extension_page_title)}},
"size": "large"
"size": "medium",
"weight": "bolder"
},
{
"type": "Input.Text",
@@ -122,9 +123,8 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
}
catch (Exception e)
{
BuiltinsExtensionHost.Instance.HideStatus(_creatingMessage);
_creatingMessage.State = MessageState.Error;
_creatingMessage.Progress = null;
_creatingMessage.Message = $"Error: {e.Message}";
}

View File

@@ -17,13 +17,15 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
{
private readonly ExtensionObject<IContentPage> _model;
private readonly Lock _commandsLock = new();
private volatile CommandSnapshot _snapshot = CommandSnapshot.Empty;
[ObservableProperty]
public partial ObservableCollection<ContentViewModel> Content { get; set; } = [];
public List<IContextItemViewModel> Commands { get; private set; } = [];
private List<IContextItemViewModel> Commands { get; } = [];
public bool HasCommands => ActualCommands.Count > 0;
public bool HasCommands => _snapshot.PrimaryCommand is not null;
public DetailsViewModel? Details { get; private set; }
@@ -31,19 +33,17 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
public bool HasDetails => Details is not null;
/////// ICommandBarContext ///////
public IEnumerable<IContextItemViewModel> MoreCommands => Commands.Skip(1);
public IReadOnlyList<IContextItemViewModel> MoreCommands => _snapshot.MoreCommands;
private List<CommandContextItemViewModel> ActualCommands => Commands.OfType<CommandContextItemViewModel>().ToList();
public bool HasMoreCommands => _snapshot.SecondaryCommand is not null;
public bool HasMoreCommands => ActualCommands.Count > 1;
public string SecondaryCommandName => _snapshot.SecondaryCommand?.Name ?? string.Empty;
public string SecondaryCommandName => SecondaryCommand?.Name ?? string.Empty;
public CommandItemViewModel? PrimaryCommand => _snapshot.PrimaryCommand;
public CommandItemViewModel? PrimaryCommand => HasCommands ? ActualCommands[0] : null;
public CommandItemViewModel? SecondaryCommand => _snapshot.SecondaryCommand;
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[1] : null;
public List<IContextItemViewModel> AllCommands => Commands;
public IReadOnlyList<IContextItemViewModel> AllCommands => _snapshot.AllCommands;
// Remember - "observable" properties from the model (via PropChanged)
// cannot be marked [ObservableProperty]
@@ -109,28 +109,14 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
return; // throw?
}
Commands = model.Commands
.ToList()
.Select<IContextItem, IContextItemViewModel>(item =>
{
if (item is ICommandContextItem contextItem)
{
return new CommandContextItemViewModel(contextItem, PageContext);
}
else
{
return new SeparatorViewModel();
}
})
.ToList();
var commands = BuildCommandViewModels(model.Commands);
InitializeCommandViewModels(commands, static contextItem => contextItem.InitializeProperties());
Commands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.InitializeProperties();
});
lock (_commandsLock)
{
ListHelpers.InPlaceUpdateList(Commands, commands);
RefreshCommandSnapshotsUnsafe();
}
var extensionDetails = model.Details;
if (extensionDetails is not null)
@@ -168,37 +154,29 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
var more = model.Commands;
if (more is not null)
{
var newContextMenu = more
.ToList()
.Select(item =>
{
if (item is ICommandContextItem contextItem)
{
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
}
else
{
return new SeparatorViewModel();
}
})
.ToList();
var newContextMenu = BuildCommandViewModels(more);
InitializeCommandViewModels(newContextMenu, static contextItem => contextItem.SlowInitializeProperties());
lock (Commands)
List<IContextItemViewModel> removedItems;
lock (_commandsLock)
{
ListHelpers.InPlaceUpdateList(Commands, newContextMenu);
ListHelpers.InPlaceUpdateList(Commands, newContextMenu, out removedItems);
RefreshCommandSnapshotsUnsafe();
}
Commands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(contextItem =>
{
contextItem.SlowInitializeProperties();
});
CleanupCommandViewModels(removedItems);
}
else
{
Commands.Clear();
List<IContextItemViewModel> removedItems;
lock (_commandsLock)
{
removedItems = [.. Commands];
Commands.Clear();
RefreshCommandSnapshotsUnsafe();
}
CleanupCommandViewModels(removedItems);
}
UpdateProperty(nameof(PrimaryCommand));
@@ -206,6 +184,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
UpdateProperty(nameof(SecondaryCommandName));
UpdateProperty(nameof(HasCommands));
UpdateProperty(nameof(HasMoreCommands));
UpdateProperty(nameof(MoreCommands));
UpdateProperty(nameof(AllCommands));
DoOnUiThread(
() =>
@@ -243,6 +222,72 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
});
}
private List<IContextItemViewModel> BuildCommandViewModels(IContextItem[]? items)
{
if (items is null)
{
return [];
}
return items
.Select<IContextItem, IContextItemViewModel>(item =>
{
if (item is ICommandContextItem contextItem)
{
return new CommandContextItemViewModel(contextItem, PageContext);
}
return new SeparatorViewModel();
})
.ToList();
}
private static void InitializeCommandViewModels(IEnumerable<IContextItemViewModel> commands, Action<CommandContextItemViewModel> initialize)
{
foreach (var contextItem in commands.OfType<CommandContextItemViewModel>())
{
initialize(contextItem);
}
}
private static void CleanupCommandViewModels(IEnumerable<IContextItemViewModel> commands)
{
foreach (var contextItem in commands.OfType<CommandContextItemViewModel>())
{
contextItem.SafeCleanup();
}
}
private void RefreshCommandSnapshotsUnsafe()
{
var allCommands = (IContextItemViewModel[])[.. Commands];
var moreCommands = allCommands.Length > 1
? allCommands[1..]
: [];
CommandContextItemViewModel? primary = null;
CommandContextItemViewModel? secondary = null;
foreach (var item in allCommands)
{
if (item is not CommandContextItemViewModel command)
{
continue;
}
if (primary is null)
{
primary = command;
}
else if (secondary is null)
{
secondary = command;
break;
}
}
_snapshot = new(allCommands, moreCommands, primary, secondary);
}
// InvokeItemCommand is what this will be in Xaml due to source generator
// this comes in on Enter keypresses in the SearchBox
[RelayCommand]
@@ -270,12 +315,15 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
Details?.SafeCleanup();
Commands
.OfType<CommandContextItemViewModel>()
.ToList()
.ForEach(item => item.SafeCleanup());
List<IContextItemViewModel> removedItems;
lock (_commandsLock)
{
removedItems = [.. Commands];
Commands.Clear();
RefreshCommandSnapshotsUnsafe();
}
Commands.Clear();
CleanupCommandViewModels(removedItems);
foreach (var item in Content)
{
@@ -290,4 +338,25 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
model.ItemsChanged -= Model_ItemsChanged;
}
}
/// <summary>
/// Immutable bundle of derived command state, published atomically via a
/// single volatile write so readers never see a torn snapshot.
/// </summary>
private sealed class CommandSnapshot(
IContextItemViewModel[] allCommands,
IContextItemViewModel[] moreCommands,
CommandContextItemViewModel? primaryCommand,
CommandContextItemViewModel? secondaryCommand)
{
public static CommandSnapshot Empty { get; } = new([], [], null, null);
public IContextItemViewModel[] AllCommands { get; } = allCommands;
public IContextItemViewModel[] MoreCommands { get; } = moreCommands;
public CommandContextItemViewModel? PrimaryCommand { get; } = primaryCommand;
public CommandContextItemViewModel? SecondaryCommand { get; } = secondaryCommand;
}
}

View File

@@ -0,0 +1,302 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Concurrent;
using Microsoft.CmdPal.Common.Helpers;
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// An elastic pool of dedicated background threads for running blocking work
/// off the ThreadPool. Starts with <c>minThreads</c> always-alive threads and
/// expands up to <c>maxThreads</c> on demand. Threads above the minimum exit
/// automatically after <c>idleTimeout</c> with no work. Items are processed
/// FIFO; cancelled items are skipped at dequeue time.
/// </summary>
internal sealed partial class DedicatedThreadPool : IDisposable
{
private const int DrainTimeoutMs = 3000;
private readonly BlockingCollection<Action> _workQueue = new();
private readonly int _minThreads;
private readonly int _maxThreads;
private readonly TimeSpan _idleTimeout;
private readonly string _name;
// Total live threads (Interlocked). Owned by the thread that wins the CAS.
private int _threadCount;
// Threads currently blocked in TryTake waiting for work (Interlocked).
// Used as the expansion trigger: if zero, all threads are busy.
private int _idleCount;
// Ever-increasing counter for unique thread names across expand/shrink cycles.
private int _nextThreadId;
private InterlockedBoolean _disposed;
public DedicatedThreadPool(int minThreads, int maxThreads, string name = "DedicatedWorker", TimeSpan? idleTimeout = null)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minThreads);
ArgumentOutOfRangeException.ThrowIfLessThan(maxThreads, minThreads);
_minThreads = minThreads;
_maxThreads = maxThreads;
_name = name;
_idleTimeout = idleTimeout ?? TimeSpan.FromSeconds(30);
_threadCount = minThreads;
for (var i = 0; i < minThreads; i++)
{
StartThread();
}
}
private void StartThread()
{
var id = Interlocked.Increment(ref _nextThreadId);
var thread = new Thread(WorkerLoop)
{
IsBackground = true,
Name = $"{_name}-{id}",
Priority = ThreadPriority.BelowNormal,
};
thread.Start();
}
private void WorkerLoop()
{
while (true)
{
Interlocked.Increment(ref _idleCount);
bool got;
Action? action;
try
{
got = _workQueue.TryTake(out action, _idleTimeout);
}
catch (ObjectDisposedException)
{
// Pool was disposed while we were waiting.
Interlocked.Decrement(ref _idleCount);
Interlocked.Decrement(ref _threadCount);
return;
}
Interlocked.Decrement(ref _idleCount);
if (got)
{
try
{
action!();
}
catch (Exception)
{
// QueueAsync wraps work in its own try-catch, so this should
// never fire. Keep the thread alive defensively.
}
continue;
}
// TryTake timed out (no work for idleTimeout).
if (_workQueue.IsCompleted)
{
break;
}
// Try to shrink: exit if we're above the minimum.
// CAS ensures exactly one thread wins each decrement race.
while (true)
{
var count = _threadCount;
if (count <= _minThreads)
{
break; // At minimum — stay alive.
}
if (Interlocked.CompareExchange(ref _threadCount, count - 1, count) == count)
{
return; // Decremented successfully — this thread exits.
}
// Another thread changed _threadCount concurrently; retry.
}
}
Interlocked.Decrement(ref _threadCount);
}
/// <summary>
/// Queue a blocking work item. Returns a <see cref="Task"/> that
/// completes when the work finishes on a dedicated thread.
/// If <paramref name="cancellationToken"/> is already cancelled when
/// the item reaches the front of the queue, it is skipped immediately.
/// Spawns an extra thread (up to <c>maxThreads</c>) if all current
/// threads are occupied.
/// </summary>
public Task QueueAsync(Action work, CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
try
{
_workQueue.Add(
() =>
{
if (cancellationToken.IsCancellationRequested)
{
tcs.TrySetCanceled(cancellationToken);
return;
}
try
{
work();
tcs.TrySetResult();
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
},
cancellationToken);
// If no thread is idle, all are blocked in COM calls — try to expand.
if (Volatile.Read(ref _idleCount) == 0)
{
TryExpand();
}
}
catch (OperationCanceledException)
{
tcs.TrySetCanceled(cancellationToken);
}
catch (ObjectDisposedException)
{
tcs.TrySetCanceled(CancellationToken.None);
}
catch (InvalidOperationException)
{
// CompleteAdding was called — pool is shutting down.
tcs.TrySetCanceled(CancellationToken.None);
}
return tcs.Task;
}
/// <summary>
/// Queue a blocking work item. Returns a <see cref="Task{T}"/> that
/// completes when the work finishes on a dedicated thread.
/// If <paramref name="cancellationToken"/> is already cancelled when
/// the item reaches the front of the queue, it is skipped immediately.
/// Spawns an extra thread (up to <c>maxThreads</c>) if all current
/// threads are occupied.
/// </summary>
public Task<T> QueueAsync<T>(Func<T> work, CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
try
{
_workQueue.Add(
() =>
{
if (cancellationToken.IsCancellationRequested)
{
tcs.TrySetCanceled(cancellationToken);
return;
}
try
{
tcs.TrySetResult(work());
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
},
cancellationToken);
// If no thread is idle, all are blocked in COM calls — try to expand.
if (Volatile.Read(ref _idleCount) == 0)
{
TryExpand();
}
}
catch (OperationCanceledException)
{
tcs.TrySetCanceled(cancellationToken);
}
catch (ObjectDisposedException)
{
tcs.TrySetCanceled(CancellationToken.None);
}
catch (InvalidOperationException)
{
// CompleteAdding was called — pool is shutting down.
tcs.TrySetCanceled(CancellationToken.None);
}
return tcs.Task;
}
/// <summary>
/// Attempt to spawn one additional thread, up to <c>maxThreads</c>.
/// CAS on <c>_threadCount</c> ensures at most one thread wins per slot.
/// </summary>
private void TryExpand()
{
if (_disposed.Value)
{
return;
}
while (true)
{
var count = _threadCount;
if (count >= _maxThreads)
{
return;
}
if (Interlocked.CompareExchange(ref _threadCount, count + 1, count) == count)
{
StartThread();
return;
}
// Another concurrent expand won this slot; recheck the ceiling.
}
}
public void Dispose()
{
if (!_disposed.Set())
{
return;
}
_workQueue.CompleteAdding();
// Give worker threads a chance to drain remaining items and exit.
// After CompleteAdding, idle threads see IsCompleted and exit
// quickly, but threads blocked in long COM calls won't return
// until their call finishes — don't wait forever.
var deadline = Environment.TickCount64 + DrainTimeoutMs;
var spin = default(SpinWait);
while (Volatile.Read(ref _threadCount) > 0 && Environment.TickCount64 < deadline)
{
spin.SpinOnce();
}
// Dispose the queue even if threads are still alive. Threads
// blocked in TryTake will get ObjectDisposedException and exit
// via the catch in WorkerLoop. Threads busy in action!() will
// finish their item, then hit ObjectDisposedException on the
// next TryTake and exit.
_workQueue.Dispose();
}
}

Some files were not shown because too many files have changed in this diff Show More