Compare commits

...

104 Commits

Author SHA1 Message Date
Michael Jolley
1d11b732b7 [CmdPal] Fix memory leak in PerformanceWidgetsPage network band items (#48880)
## Summary

Fixes a memory leak in the Performance Monitor dock extension where
`GetItems()` created **new** `ListItem` instances for `_networkUpItem`
and `_networkDownItem` on every call.

## Problem

When the dock subscribes to `ItemsChanged` and calls `GetItems()` to
refresh, the band page path allocates 2 new `ListItem` objects each time
— the old ones are replaced in the fields but never collected (they
remain referenced by the `DockItemViewModel` wrappers until the next
refresh cycle). Under normal operation this leaks ~2 objects/second
indefinitely.

## Fix

Move `_networkUpItem`/`_networkDownItem` creation into the constructor
(matching the pattern used by CPU, Memory, GPU, and Battery items).
`GetItems()` now returns stable references. The `Updated` event handler
already updates their `.Title` properties, which propagates to the UI
via `PropChanged` → `CommandItemViewModel.Model_PropChanged`.

## Validation

- Build succeeds (`Microsoft.CmdPal.Ext.PerformanceMonitor.csproj`)
- Network up/down band items still receive title updates via the
existing `Updated` handler
- No `RaiseItemsChanged()` needed — `ListItem.Title` setter fires
`PropChanged`, which `DockItemViewModel` already observes

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 28e078897a)
2026-06-26 13:35:45 +08:00
Mike Griese
ab4792a4b8 Revert "[CmdPal][Dock] Fix performance meter showing '???' after restart" (#48835)
Reverts microsoft/PowerToys#48682

I'm quite sure that OP did not build or test these changes, and they
should not have merged.

(cherry picked from commit 386a16ff94)
2026-06-26 13:35:45 +08:00
Niels Laute
dc3a9da968 [Shortcut Guide] Prevent overlay crash on section navigation (#48448) (#48481)
## Summary of the Pull Request

Shortcut Guide overlay crashes and closes when navigating between
sidebar sections (e.g. clicking PowerToys, then clicking the Windows
icon to come back). Repro and crash logs in #48448. Crash logs in #48441
show the same propagation path plus a follow-up access violation in
coreclr, indicating exceptions that escape local catches.

This PR fixes the immediate navigation race and adds broader crash
hardening so future exceptions on the UI/background threads are logged
instead of tearing down the overlay.

### Navigation-race fix (commit 1)

Root cause: `WindowSelector_SelectionChanged` calls
`App.TaskBarWindow.Activate()` and then `SetWindowPosition()`
synchronously. `Activate()` runs a reentrant `Window_Activated` →
`BringToFront` → `TaskbarWindow.Activated` chain that can leave
`App.TaskBarWindow.AppWindow` momentarily null, so `SetWindowPosition`
throws `NullReferenceException`.

Because the initial `SelectedItem = MenuItems[0]` is set from
`SetNavItems`, the NRE bubbles up into `InitializeNavItemsAsync`'s catch
block — which sets `_closeType = "InitializationFailed"` and closes the
window. That matches both user-visible symptoms: the overlay "flashes
and disappears" (#48441) on open and "closes when clicking the Windows
icon" (#48448).

Edits in
`src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/MainWindow.xaml.cs`:

- **`SetWindowPosition`**: null-guard `App.TaskBarWindow?.AppWindow` and
skip the taskbar-overlap height adjustment when it is not currently
observable. Wrap the body in `try`/`catch` with `Logger.LogError` so any
future positioning hiccup keeps the previous layout instead of taking
down the overlay.
- **`WindowSelector_SelectionChanged`**: null-guard
`App.TaskBarWindow?.Hide()` / `Activate()` and wrap the body in
`try`/`catch`. This breaks the propagation path that lets a navigation
exception reach `InitializeNavItemsAsync`'s "fatal init failure" branch.

### Crash hardening (commit 2)

Additional defensive changes to cover other unguarded paths surfaced
while reviewing #48441:

- **`App.xaml.cs`**: register `App.UnhandledException`,
`AppDomain.CurrentDomain.UnhandledException`, and
`TaskScheduler.UnobservedTaskException` so a stray exception (e.g. an IO
failure during a fire-and-forget UI handler, or a background `Task`
fault) gets logged instead of tearing the process down with an access
violation in coreclr. Mirrors what Peek, AdvancedPaste, and CmdPal
already do.
- **`App.OnLaunched`**: wrap the launch sequence in `try`/`catch` and
exit cleanly with an error log on failure (`LoadData` / `MainWindow` /
`TaskbarWindow` ctors and `Activate` are all reachable failure points).
- **`App.LoadData`**: broaden the `Pinned.json` deserialize catch to
also handle `IOException` / `UnauthorizedAccessException`, and guard the
round-trip `SaveSettings` call as best-effort with a warning log.
- **`PinnedShortcutsHelper.Save`**: catch `IOException` /
`UnauthorizedAccessException` / `JsonException` and log; `Pin` / `Unpin`
runs from a synchronous UI handler so an unguarded `File.WriteAllText`
would tear down the overlay on any disk hiccup.
- **`TaskbarWindow.UpdateTasklistButtons`**: move the `AppWindow.Move`
calls inside the existing `try` block, null-guard
`App.MainWindow?.AppWindow` up front, and wrap the whole body so the
method (which runs from the ctor and from `Activated`) cannot tear the
overlay down when `MainWindow` is in a transient state.

## PR Checklist

- [x] Closes: #48448
- [x] Likely also fixes: #48441
- [x] **Communication:** Defensive fix to known crash paths; no API
change.
- [ ] **Tests:** No automated tests added — the failures are reentrancy
/ timing races that are hard to deterministically trigger in CI.
Validated with a synthetic repro (see below).
- [x] **Localization:** N/A — only logger strings.
- [x] **Dev docs:** N/A
- [x] **New binaries:** N/A

## Detailed Description of the Pull Request / Additional comments

The fix is intentionally defensive (rather than restructuring the
reentrant activation flow) because the legacy taskbar UIA enumeration
(`TasklistPositions.GetButtons`) on Windows 10 is what most reliably
widens the race window and is out of scope to redesign for a hotfix.

## Validation Steps Performed

- Build: `tools\build\build.cmd` from
`src\modules\ShortcutGuide\ShortcutGuide.Ui` — exit 0, errors log empty
for both commits.
- Synthetic repro: temporarily injected `throw new
NullReferenceException()` at the top of `SetWindowPosition` to exercise
the exact propagation path the reporter's log shows (`SetWindowPosition`
→ `SelectionChanged` → `set_SelectedItem` →
`InitializeNavItemsAsync.catch` → `Close("InitializationFailed")`).
- Before fix: overlay flashes and closes, log shows `Failed to
initialize navigation items.` with the NRE.
- After fix: overlay stays open, page navigates, log shows `Failed to
set Shortcut Guide window position; keeping previous layout.` from the
new `catch`.
- Did not reproduce the live race on a Win11 dev box; the reporter's
repro is Windows 10 19045 / Microsoft Store install where the legacy
taskbar UIA timing makes the reentrant chain more likely to expose the
null.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 502dc40aa4)
2026-06-22 17:15:23 +08:00
moooyo
baee472288 [PowerDisplay] Detect built-in panel when driven by the discrete GPU (#48637)
On dual-GPU laptops, Power Display stopped detecting the built-in panel
(and adjusting its brightness) when the **discrete GPU** drives the
display — it showed "can't detect the display". This fixes that by
classifying displays by **capability** (does WMI brightness work on it?)
instead of by the nominal `OutputTechnology` value, which the discrete
GPU misreports for the internal panel.

- [x] Closes: #48587
- [x] **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 (no
new user-facing strings added)
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places

On a hybrid / MUX laptop, when the **discrete GPU** drives the built-in
eDP panel, `QueryDisplayConfig` reports the panel's
`DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY` as `DISPLAYPORT_EXTERNAL` (`10`)
instead of the `INTERNAL` flag (`0x80000000`) it reports under the
integrated GPU. It is the *same physical panel* (same EDID) — only the
reported connector type changes with the active GPU.

PR #47740 introduced a strict classifier: `OutputTechnology` →
internal/external, then **internal → WMI-only, external → DDC/CI-only,
with no fallback**. So under the discrete GPU the built-in panel was
classified *external* and sent to DDC/CI only — but a laptop eDP panel
does not speak DDC/CI, so it was dropped and Power Display reported it
couldn't detect any monitor. (`WmiMonitorBrightness` still exposes that
panel regardless of which GPU drives it, so the panel was actually
controllable — it just never got routed to WMI.)

- **`MonitorManager`** now runs **WMI discovery first** over the full
`QueryDisplayConfig` inventory. Every display `WmiMonitorBrightness`
exposes is treated as internal (WMI-controlled); whatever WMI does
**not** claim is routed to DDC/CI. The `OutputTechnology`-based
classifier is gone.
- **`WmiController`** matches the system-wide `WmiMonitorBrightness`
results against the full inventory by `Monitor.Id`. The persisted
`Monitor.Id` is still taken from the matched `DevicePath`
(byte-identical to the DDC route and to prior releases), so saved
brightness/per-monitor settings survive upgrades.
- New **`MonitorIdentity.FromInstanceName`** reduces a WMI
`InstanceName` to the same canonical `Monitor.Id` as `FromDevicePath`;
the separate `PnpHardwareKey` helper is removed.
- **Deleted** `DisplayClassifier` and `MonitorDisplayInfo.IsInternal`
(net ~150 fewer lines).

A monitor that exposes **both** `WmiMonitorBrightness` **and** DDC/CI is
now controlled via WMI only and won't get DDC-only features (contrast /
volume / input source / color temperature / power). This is uncommon
(typical laptop panels are WMI-only; typical external monitors are
DDC-only) and is a deliberate decision: it removes the entire class of
`OutputTechnology` misclassification bugs while keeping the performance
win of not DDC-probing internal panels.

- Built the Power Display app (`PowerDisplay.csproj`) and
`PowerDisplay.Lib.UnitTests` (x64 / Debug) with MSBuild — both succeed,
including after merging latest `main` (Windows App SDK 2.2.0).
- Ran the unit test suite: **128/128 pass**, including new
`FromInstanceName` tests — the `FromInstanceName == FromDevicePath`
equivalence invariant and a concrete #48587 regression case (the BOE
panel reported as `OutputTechnology=10`).
- Traced the fix against the reporter's diagnostic logs: the panel that
previously went `OutputTechnology=10 → External → DDC → dropped` is now
claimed by WMI and controllable.
- Reviewed the diff for regressions (Monitor.Id persistence, monitor
blacklist, mirror mode, dual-internal-panel devices, external-only
desktops).

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 32ad98a0dd)
2026-06-22 16:30:19 +08:00
Bryce Cindrich
142833d49a fix(shortcut-guide): use <N> token for literal digit keys in manifests (#48757)
## Summary of the Pull Request

Converts the literal-digit shortcut keys in the bundled Shortcut Guide
manifests to the `<N>` special-key convention, so they render as the
correct number.

Per the manifest spec, a bare number in `Keys:` is a virtual-key code. A
literal digit key authored as a bare number is therefore misread (VK `9`
is Tab, VK `1` is the left mouse button, VK `0` is undefined) and
renders incorrectly. The fix authors these as the `<N>` token (for
example `"<9>"`), which `KeyVisual` strips to display the digit.

This is a data-only change: **91 literal-digit keys across 14
manifests** become `<N>` tokens. No code or doc changes; the renderer
and converter already support `<N>`, and the convention is documented in
the spec.

Follow-up to #48461, which introduced and documented the `<N>`
convention (per @noraa-junker's request for a separate PR to fix the
remaining manifests). Together with #48461 this resolves the rendering
reported in #48460.

Files touched (all under
`src/modules/ShortcutGuide/ShortcutGuide.Ui/Assets/ShortcutGuide/Manifests/`):
Adobe.Illustrator (18), Adobe.Photoshop (17), SlackTechnologies.Slack
(13), Adobe.InDesign (11), JetBrains.IntelliJIDEA.Community (11),
BlenderFoundation.Blender (3), Figma.Figma (3), Google.Chrome (3),
Microsoft.Edge (3), Microsoft.VisualStudioCode (3), Mozilla.Firefox (3),
+WindowsNT.Notepad (1), Adobe.AfterEffects (1), GIMP.GIMP (1).

## PR Checklist

- [ ] **Closes:** N/A (follow-up to #48461; contributes to #48460)
- [ ] **Communication:** discussed in #48461; @noraa-junker requested
this separate PR.
- [ ] **Tests:** N/A for data; the converter and `<N>` convention are
unit-tested in #48461. Validated here by deserializing every manifest
with YamlDotNet (see below).
- [x] **Localization:** unchanged; these are per-language manifest
files.
- [ ] **Dev docs:** N/A (the `<N>` convention is documented in the spec
via #48461).
- [ ] **New binaries:** N/A.
- [ ] **Documentation updated:** N/A.

## Detailed Description of the Pull Request / Additional comments

Each change wraps a single bare digit in angle brackets, for example:

```yaml
          Keys:
-            - 9
+            - "<9>"
```

Quoted tokens (`"<9>"`) are used to match the dominant special-token
style already in the manifests (`"<Enter>"`, `"<Down>"`, etc.).

Note (out of scope): `Adobe.Photoshop.en-US.yml` has a shortcut with an
empty `Name: ""` (around line 799). That is a pre-existing data issue
unrelated to digit rendering; the digit is still converted, and the
empty name is left as-is.

## Validation Steps Performed

- Confirmed the diff touches only the 14 manifests, 91 insertions and 91
deletions, with each changed line being a digit wrapped as `"<N>"` (no
whitespace, indentation, or encoding churn).
- Confirmed zero bare-digit `Keys` entries remain and exactly 91 new
`"<N>"` tokens exist.
- Deserialized all 32 manifests with YamlDotNet (the same library the
app uses at runtime): 0 parse errors.
- Rendering behavior for `<N>` is already verified in #48461 (the
renderer strips the brackets to show the digit).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit cabb71108a)
2026-06-22 16:29:53 +08:00
Bryce Cindrich
46d594a7f5 feat(shortcut-guide): add Postman manifest and fix numbered-key display (#48461)
## Summary of the Pull Request

Adds a Shortcut Guide manifest for **Postman** and fixes a rendering bug
where single-digit keys in manifests displayed incorrectly.

- **Fix numbered-key rendering** —
`src/modules/ShortcutGuide/ShortcutGuide.Ui/Converters/ShortcutDescriptionToKeysConverter.cs`:
a single digit (`0`–`9`) in a manifest's `Keys` was treated as a Windows
virtual-key code instead of the literal digit. Since VK `1` is the left
mouse button, VK `9` is Tab, and VK `0` is undefined, shortcuts such as
`Ctrl+0` (reset zoom) and `Ctrl+9` (last tab) rendered as
blank/incorrect glyphs. Single digits are now rendered as the literal
character.
- **Add Postman shortcuts** —
`src/modules/ShortcutGuide/ShortcutGuide.Ui/Assets/ShortcutGuide/Manifests/Postman.Postman.en-US.yml`:
new manifest for `Postman.exe` covering Tabs, Sidebar, Request,
Interface, Window and modals, and Console. Auto-included via the
existing `Manifests/*.yml` glob in `ShortcutGuide.Ui.csproj`.
- **Show tab-number ranges** — Edge, Chrome, Firefox, and Postman
manifests: the "switch to a specific tab" entry used the literal key
`1`, which (after the fix above) read as `Ctrl + 1`. It now uses a `1 -
8` range so the keycap conveys "any tab number 1 through 8". The
separate "last tab" (`9`) and "reset zoom" (`0`) entries remain literal
single keys.
- **Add unit tests** — new `ShortcutGuide.UnitTests` (MSTest) project
covering `ShortcutDescriptionToKeysConverter.GetKeysList`, including the
single-digit regression.

## PR Checklist

- [x] Closes: #48460
- [ ] **Communication:** I've discussed this with core contributors
already. <!-- Filed #48460; the v0.100 announcement invites app-shortcut
contributions via PR. -->
- [x] **Tests:** Added/updated and all pass <!-- New
ShortcutGuide.UnitTests (MSTest); 8 tests pass locally via
vstest.console. -->
- [x] **Localization:** All end-user-facing strings can be localized
<!-- Shortcut names live in per-language manifest files (`*.en-US.yml`);
other locales fall back to en-US, consistent with existing manifests.
-->
- [ ] **Dev docs:** Added/updated <!-- N/A: no behavior requiring
dev-doc changes. -->
- [ ] **New binaries:** Added on the required places <!-- N/A: the new
manifest is a data asset under an already-shipped, globbed folder. The
new test project is auto-discovered by the existing `**\*UnitTest*.dll`
VSTest glob, so no CI pipeline change is required. -->
- [ ] **Documentation updated:** <!-- N/A -->

## Detailed Description of the Pull Request / Additional comments

The Shortcut Guide displays per-app shortcuts from YAML manifests,
matched to the foreground window via `WindowFilter`. Keys are converted
to keycaps by `ShortcutDescriptionToKeysConverter`. Numeric key strings
were unconditionally parsed as virtual-key codes, so literal-digit
shortcuts rendered wrong. The fix adds a `>= 0 and <= 9` case that emits
the digit character as-is; non-digit numeric codes (arrows, etc.) are
unchanged.

The new Postman manifest exercises this with `Ctrl+0` / `Ctrl+9`. The
browser/Postman "specific tab" entries were updated from the literal `1`
to the `1 - 8` range string, rendered verbatim by `KeyVisual` (the same
path used by the existing `Number (1-9)` key in the Windows Explorer
manifest).

A new `ShortcutGuide.UnitTests` (MSTest) project covers the converter:
single digits render literally (regression test), modifier ordering,
non-numeric passthrough (e.g. `1 - 8`), and arrow-key VK mapping.

## Validation Steps Performed

Built and ran locally (x64 Debug):

- Built `ShortcutGuideModuleInterface`, `ShortcutGuide.Ui`, and
`ShortcutGuide.IndexYmlGenerator`; launched the Debug `PowerToys.exe`.
- Triggered Shortcut Guide (`Win+Shift+/`) with **Postman** focused: the
Postman section renders with all categories, and `Ctrl+1` / `Ctrl+9` /
`Ctrl+0` display correctly (previously blank/incorrect).
- Verified the "specific tab" entry renders as `Ctrl + 1 - 8` in
**Edge**, **Chrome**, **Firefox**, and **Postman**.
- Built `ShortcutGuide.UnitTests` and ran via `vstest.console.exe`:
**8/8 tests pass**.

<img width="845" height="1432" alt="PowerToys Shortcut Guide Running
Postman"
src="https://github.com/user-attachments/assets/6359617e-3e2c-48b0-8005-b3684594ec94"
/>

Co-Authored-By: Claude Opus 4.8

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit a0e53de825)
2026-06-22 16:29:52 +08:00
Dave Rayment
673a41512c [ColorPicker] Fix the main window UI appearing in the zoomed-in view (#48762)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

- [x] Closes: #37801
<!-- - [ ] 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
This uses `SetWindowAffinity()` with the `WDA_EXCLUDEFROMCAPTURE`
constant on the main Color Picker window to ensure the ZoomWindow bitmap
creation excludes the corner of the picker UI. The ability to capture
the picker window is restored immediately after, so it's still visible
in Snipping Tool, Remote Desktop and other tools.

The fix uses a new helper with simple `Include()` / `Exclude()` methods
as an adapter to the native code. This may potentially be included in
Common if other utilities needed it.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
1. Repro'd the original issue on the current Color Picker release:
<img width="262" height="263" alt="image"
src="https://github.com/user-attachments/assets/bc12b2f8-b67f-4b56-a803-57ab2fe0fa17"
/>

2. Confirmed that the fix worked and normal window affinity was restored
(otherwise I would not have been able to snip this):
<img width="262" height="262" alt="image"
src="https://github.com/user-attachments/assets/bc336ad0-4114-49fa-8440-b78466de363f"
/>

3. Repeated zooming in and out over a normal session.

(cherry picked from commit eab305334b)
2026-06-22 16:29:52 +08:00
Eymard Silva
f57062c206 Fix VS Code Workspaces shared storage lookup (#47505)
## Summary

Fixes VS Code Workspaces recent entries discovery after VS Code 1.118
moved `history.recentlyOpenedPathsList` to the shared application
storage database.

The plugin now probes both the legacy `User/globalStorage/state.vscdb`
database and the new shared storage database for VS Code Stable,
Insiders, Exploration, VSCodium, VSCodium Insiders, and portable
installs.

It also deduplicates results that may appear in both locations.

Fixes #47445

## Validation

- Reviewed the change against the issue's documented VS Code 1.118
storage paths.
- Attempted local focused build from
`src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces`
with `tools/build/build.cmd -Platform x64 -Configuration Debug`.
- Build was blocked by local environment/tooling issues unrelated to
this C# change: missing/invalid VC tooling/Windows SDK.

(cherry picked from commit e1b1a8d7ed)
2026-06-22 16:29:52 +08:00
Mike Griese
f339b48324 CmdPal: Only list available docks when pinning (#48723)
This is a totally minor nitpick.

I don't have the dock enabled on all my displays. But the current "pin
to dock" dialog lets me pin it to a display I don't have the dock on.
When that happens, it effectively results in _nothing_ happening. Not
great.

This PR mitigates that situation, but only listing the enabled docks
when pinning.

(cherry picked from commit 6dcda8a6aa)
2026-06-22 16:29:52 +08:00
ABHIJEET KALE
f590871d6d [CmdPal][Dock] Fix performance meter showing '???' after restart (#48682)
This PR fixes Issue #48680 where the CmdPal dock performance meter shows
'???' after restart.

**Root cause**: The PerformanceWidgetsPage initially returns items with
a placeholder title ('???') because ContentData hasn't loaded yet. The
DataManager timer starts when the dock subscribes to ItemsChanged, and
the Updated event fires 1 second later with real data. However, the
Updated event handlers were updating ListItem.Title directly without
calling RaiseItemsChanged() on the page, so the dock was never notified
that the items had changed and continued to display the stale '???'
title.

**Fix**: Added a RaiseItemsChanged() call to each of the 5 Updated event
handlers (CPU, Memory, Network, GPU, Battery) after the item titles are
updated. This causes the dock to re-call GetItems() and refresh the
displayed titles.

Fixes #48680

(cherry picked from commit 0d12a62abb)
2026-06-22 16:29:52 +08:00
William
bdcedb142e Changed sleep icon to hibernation icon in Command Palette (#48689)
<!-- 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
Changed the icon from the sleep icon to the hibernate icon, which fixes
the issue in #48535

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

- [x] Closes: #48535
## Detailed Description of the Pull Request / Additional comments
The icon was previously the sleep icon, and I have changed it to the
hibernate icon.

## Validation Steps Performed
I have visually checked that the icon is now updated to the correct
icon.

(cherry picked from commit 2125d739a3)
2026-06-22 16:29:52 +08:00
Mike Griese
7da85cac40 CmdPal: fix initializing the Run history in AOT builds (#48463)
Regressed in one of the last two releases.

The problem was that `history.ToImmutableList()` ran on the projected
`IVector<string>`. `ImmutableList.CreateRange` checks for
`IReadOnlyCollection<string>`, and resolving that interface on a WinRT
object requires a helper type that AOT can't generate.

So we have to just _not do that_.

Closes #48445

(cherry picked from commit fb0f4292eb)
2026-06-22 16:29:52 +08:00
Clint Rutkas
750ef385b8 [QuickAccess] Suppress unhandled XAML exceptions in flyout host (#48457)
## Summary

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

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

## Changes

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

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

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

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

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

## Why both handlers, not just one

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

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

## Testing

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

## Risk

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

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

---

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit ab4947579b)
2026-06-22 16:29:51 +08:00
Matheus Mol
7279bfd9ec [KBM] Fix modifier key remapped to non-modifier delivering WM_SYSKEYDOWN (#47192)
## Summary of the Pull Request

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

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

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

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

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

## Validation Steps Performed

1. Remapped Left Alt → Backspace in Keyboard Manager
2. Opened a text editor, typed text, pressed the remapped key
3. Confirmed single characters are deleted instead of whole words
4. All 93 unit tests pass (KeyboardManager.Engine.UnitTests)

(cherry picked from commit 5688441127)
2026-06-22 16:29:51 +08:00
Mario Hewardt
8d6bb43c26 ZoomIt - Fix race condition in audio init (#48685)
It was a race condition.

(cherry picked from commit d9216f0fc7)
2026-06-22 16:29:51 +08:00
moooyo
b82e3535da [PowerDisplay] Allow waking a monitor from standby via power-state On (#48628)
<!-- 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

Power Display could put a monitor to sleep but never wake it back up.
Selecting **On** in the per-monitor power-state list was a hard-coded
no-op, so the DDC/CI wake command (VCP `0xD6` = `0x01`) was never sent.
This removes that guard so selecting **On** wakes the display, and
cleans up the dead code/comment left behind by the original
one-directional design.

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

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

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

`MonitorViewModel.HandlePowerStateSelectionChanged` early-returned when
the selected power state was **On** (`0x01`), so `SetPowerStateAsync`
was never called for On and the wake write never reached the monitor. As
a result Power Display's power control was one-directional: it could
send Standby/Suspend/Off but could never turn a monitor back on.

The guard dates back to the very first power-state commit and was paired
with a single-monitor assumption — *"the monitor must be on to see the
UI"*, so On was treated as the always-current state and skipped. A later
change made the selection reflect the monitor's real power state (so a
monitor in the list can legitimately be asleep), and multi-monitor
support means the flyout can be shown on monitor A while the user wants
to wake monitor B. Those changes invalidated the assumption, but the
action-side guard survived a subsequent refactor.

The lower layers already do the right thing:
`MonitorManager.SetPowerStateAsync` →
`DdcCiController.SetPowerStateAsync` → `SetVcpFeatureAsync(monitor,
0xD6, value)` passes the value through unchanged, so the fix is purely
removing the UI-layer guard. DDC/CI stays reachable while the panel is
in Standby/Suspend/Off(DPM), so writing `0x01` turns it back on (this is
the same mechanism Twinkle Tray uses). `Off (Hard)` / `0x05` may still
require a physical wake on some monitors, since that state can cut the
DDC command channel.

Cleanup included in this PR:
- Removed the now-unused `PowerStateItem.PowerStateOn` constant (its
only consumer was the deleted guard).
- Removed the dead `SetPowerState` `[RelayCommand]` (the generated
`SetPowerStateCommand` had zero references — the XAML wires
`SelectionChanged`, not a command).
- Updated the `SetPowerStateAsync` doc comment from the one-directional
framing to a neutral bidirectional description.

Net change: 2 files, +5 / −24.

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

- **Build:** `MSBuild PowerDisplay.csproj -p:Platform=x64
-p:Configuration=Debug` (CoreCompile) — **0 errors / 0 warnings**.
- **Static check:** repo-wide grep confirms no remaining references to
`PowerStateOn` or `SetPowerStateCommand`; the power-state ListView binds
only `ItemsSource` + `SelectionChanged` (no `SelectedItem` binding), so
opening the flyout cannot spuriously re-fire a selection.
- **Manual (requires a DDC/CI monitor):** enable *Power state control*
for a monitor → open its flyout and select **Standby** or **Off (DPM)**
(screen blanks) → reopen the flyout and select **On** → the display
wakes.

No automated test was added: with the guard removed the handler is an
unconditional pass-through (identical in shape to
`HandleInputSourceSelectionChanged`), and it is an `async void` WinUI
event handler over real DDC/CI hardware, which is outside the
`PowerDisplay.Lib` unit-test seam.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit ff3c1f9252)
2026-06-22 16:29:13 +08:00
Boliang Zhang (from Dev Box)
d81186267d Merge main into stable for 0.100 release (rev 14) 2026-06-09 23:45:55 +08:00
Boliang Zhang (from Dev Box)
d22589bcf6 Revert "Add copy monitor diagnostics button to Power Display (#48209)"
This reverts commit 76eb6eaac5.
2026-06-05 15:42:28 +08:00
Boliang Zhang (from Dev Box)
0aed70cc87 Merge main into stable for 0.100 release (rev 13) 2026-06-05 11:33:54 +08:00
Boliang Zhang (from Dev Box)
ba70becdef Merge main into stable for 0.100 release (rev 12) 2026-06-03 17:43:12 +08:00
Mike Griese
820d98cb43 cmdpal: fix the dock window border being visible, redux (#48180)
Addenda to #47187

that fix only works if the _hwnd is already set. Actually it's crazy it
ever worked.

Tested by disconnecting and reconnecting RDP a couple times, which
pretty consistently reproduces the problem.

(cherry picked from commit c6a9ad2ad0)
2026-06-02 21:41:29 +08:00
Dustin L. Howett
cbeeefcaf9 Remove our dependency on expected-lite (#48159)
This removes our last git submodule dependency!

We were using `expected-lite` in one place, which was being compiled out
_anyway_ in favor of using `std::expected`.

(cherry picked from commit 109c63ba33)
2026-06-02 21:41:29 +08:00
Boliang Zhang
6ceb53336f Migrate spdlog from submodule to vcpkg (#48039)
## Summary

Migrate `deps/spdlog` from a git submodule to **vcpkg manifest mode**
with an overlay port pinned to the **exact same commit**
(`gabime/spdlog@616866fc`). Replaces the polyfill shim added in #47910
with a proper port-level patch.

This is the follow-up to PR #47928, which I closed after @zadjii-msft /
@DHowett clarified that the intended direction was a single combined
"move to vcpkg **and** apply a patch file" (one change, not two stepping
stones).

## Guidance honored

Per @zadjii-msft (offline):
-  Convert each submodule to vcpkg **one at a time** — this PR is
**spdlog only**. `deps/expected-lite` stays a submodule (separate PR
next).
-  Atomic commit per dep (multiple commits on the branch for review
traceability; squash on merge gives the requested single commit).
-  **Don't bump the version.** Only variable changed: submodule →
vcpkg. Same commit (`616866fc`, v1.8.5 + 38) the submodule pointed at.

Per @DHowett
([review](https://github.com/microsoft/PowerToys/pull/48039#pullrequestreview-4338835150)):
-  No vcpkg submodule — vswhere-first detection via a Terminal-style
`steps-install-vcpkg.yml` template; three-tier `VcpkgRoot` fallback (env
var → VS-shipped → runtime clone pinned to manifest baseline).

## Design

- **Repo-root manifest**: `vcpkg.json` declares only `spdlog`, with
`builtin-baseline` pinned. `vcpkg-configuration.json` registers
`deps/vcpkg-overlays/` as overlay-ports.
- **Overlay port** `deps/vcpkg-overlays/spdlog/`: `vcpkg_from_github(REF
616866fc...)` with bundled fmt preserved (`-DSPDLOG_FMT_EXTERNAL=OFF`);
the MSVC 14.51 fix from #47910 carried as a proper vcpkg patch on
`include/spdlog/fmt/bundled/format.h`.
- **vcpkg integration is global** (set in `Cpp.Build.props`, imported
via `ForceImportBeforeCppProps` for every `.vcxproj`). An earlier
attempt to make vcpkg per-project-opt-in via `deps/spdlog.props` failed
because ~85 PowerToys `.vcxproj` files import `spdlog.props` AFTER
`Microsoft.Cpp.targets`, by which point `vcpkg.props`' `ClCompile` hook
is dead-on-arrival. The trade-off (every C++ project invokes `vcpkg
install` once at build time, ~0.5 s on cache hits, manifest declares
only spdlog so install set is fixed) is documented in the expanded
`Cpp.Build.props` comment.
- **`deps/spdlog.props`** is now a thin shim that only sets the
historical `SPDLOG_*` preprocessor defines for source-compat.
- **`Cpp.Build.targets`** is a new file imported via
`ForceImportAfterCppTargets` to load `vcpkg.targets` after
`Microsoft.Cpp.targets`. A fail-fast `<Target>` errors with a clear
message if `vcpkg.props` can't be found at the resolved `VcpkgRoot`.
- **Removes** `deps/spdlog-msvc-fix/` polyfill, in-tree wrapper
`src/logging/`, spdlog submodule, the single `<ProjectReference>` in
`logger.vcxproj`, plus 3 `.slnf` refs and 2 `.slnx` refs
(`PowerToys.slnx` + `installer/PowerToysSetup.slnx`), plus 3 hard-coded
`..\deps\spdlog\include` entries in `<AdditionalIncludeDirectories>`.
- **CI**: new reusable `.pipelines/v2/templates/steps-install-vcpkg.yml`
(vswhere-first, manifest-baseline-pinned fallback clone, respects
`useVSPreview`). Gated `Cache@2` for `%LOCALAPPDATA%\vcpkg\archives`
keyed on overlay-port contents. Same vcpkg detection added to
`tools\build\build-essentials.ps1` for local devs.

## Verification

Local build matrix (all 4 configs of `logger.vcxproj` and a
representative late-import consumer):

| Config | Result | Notes |
|--------|--------|-------|
| Release \| x64    |  | vcpkg install ~21 s, `logger.lib` produced |
| Debug \| x64 |  | **Validates patch fixes the actual MSVC 14.51 bug**
(`_ITERATOR_DEBUG_LEVEL > 0` → `_SECURE_SCL`) |
| Release \| ARM64 |  | vcpkg cross-installs `arm64-windows-static`
spdlog in ~16 s |
| Debug \| ARM64 |  | **Previously DISABLED for the in-tree spdlog**
(per `<Build Solution="Debug\|ARM64" Project="false" />` in
`PowerToysSetup.slnx`); this migration FIXES that latent gap |
| FancyZonesLib (Release \| x64) |  | Late-import-pattern consumer;
previously broke in v2 |

Full PowerToys CI (x64 + arm64 + CmdPal SDK + all GitHub Actions checks)
green.

**Consumer audit**: 72 `.vcxproj` files reference `logger.vcxproj`; all
72 also import `deps/spdlog.props`. No transitive-link breakage.

## Out of scope (intentional)

- `deps/expected-lite` migration — next PR per "one-at-a-time" rule.
- Remote vcpkg binary cache (Azure Artifacts NuGet feed). Local pipeline
`Cache@2` works for now, but a remote feed survives across pipelines and
is the long-term answer. Happy to split this into a follow-up.

## Notes for review

- Patch in the overlay port is identical content to PR #47928's patch
but regenerated with LF line endings (vcpkg's `vcpkg_apply_patches` is
strict; no `--ignore-whitespace`).
- Once PowerToys eventually bumps spdlog past v1.14 (which ships fmt
10.2 and drops the affected code path), the overlay port can be deleted
and we can use upstream vcpkg's `spdlog` directly.
- Re. official-release pipelines and terrapin / less-restricted network
isolation: VS-shipped vcpkg is the primary path (no network); the
fallback clone is only exercised when VS doesn't ship vcpkg. Happy to
wire terrapin into the fallback as a follow-up if the official build
template needs it.

Closes the work tracked in #47928 (which was closed unmerged).

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
(cherry picked from commit 8a7933c0b2)
2026-06-02 21:41:29 +08:00
Niels Laute
5812b55710 Rework Power Display warning dialog (#48249)
## Summary of the Pull Request

Rework the confirmation dialog shown when enabling Power Display (and
its potentially-destructive sub-features) so it is shorter, friendlier,
and consistent across all entry points. The five separate prefix-driven
variants are now a single `PowerDisplayWarningDialog` user control
selected via an enum, sharing the title, learn-more link, and
Enable/Cancel buttons. The previous hand-rolled red warning text is
replaced with a Fluent `InfoBar Severity=""Warning""`.

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

### Before
- `DangerousFeatureWarningDialog` took a resource-key prefix string and
probed up to five optional keys per variant (`_WarningHeader`,
`_WarningConfirm`, `_WarningList_Item1/2`, etc.).
- Each of the five flows (EnableModule, ColorTemperature, PowerState,
InputSource, MaxCompatibility) had a slightly different title (some
questions, some statements, mixed `Warning:` prefixes) and a hand-rolled
red `TextBlock` warning header with a `⚠️` emoji and
`SystemFillColorCriticalBrush`.
- ~30 fragmented `PowerDisplay_*_Warning*` resw keys.

### After
- New `PowerDisplayWarningDialog` selected via `PowerDisplayWarningKind`
enum (`EnableModule`, `ColorTemperature`, `PowerState`, `InputSource`,
`MaxCompatibility`).
- Shared chrome lives in the control:
  - Single title `Before you continue` for every variant.
  - `InfoBar Severity=""Warning""` replaces the hand-rolled red header.
- Learn-more `HyperlinkButton` pointing at
`aka.ms/powerToysOverview_PowerDisplay_Note` (URL is a `private const`
so translators don't see it).
  - Consistent Enable / Cancel buttons.
- Per-variant content collapses to one InfoBar message + one body
paragraph in resw (12 keys total, down from ~30). Bullets are inlined as
`• ` + newlines with `xml:space=""preserve""`.
- `PowerDisplayViewModel.ConfirmDangerousFeatureAsync` and
`TryCommitDangerousChangeAsync` now take the enum instead of a magic
string.

### Files
- **Added:** `ViewModels/PowerDisplayWarningKind.cs`,
`SettingsXAML/Views/PowerDisplayWarningDialog.xaml{,.cs}`
- **Removed:**
`SettingsXAML/Views/DangerousFeatureWarningDialog.xaml{,.cs}`
- **Updated:** `Strings/en-us/Resources.resw`,
`ViewModels/PowerDisplayViewModel.cs`,
`SettingsXAML/Views/PowerDisplayPage.xaml.cs`

## Validation Steps Performed

- Built `PowerToys.Settings.csproj` (Debug arm64) — clean.
- Manually exercised the EnableModule and MaxCompatibility flows;
verified the new title, InfoBar, body paragraph, learn-more link, and
Enable/Cancel button behavior.
- Verified `aka.ms/powerToysOverview_PowerDisplay_Note` opens in the
default browser.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 7f19817182)
2026-06-02 15:34:10 +08:00
Niels Laute
1be856c573 [KBM] Enable new editor by default (#48245)
## Summary

Make the new WinUI 3 Keyboard Manager editor the default by flipping
`useNewEditor` from `false` to `true` everywhere a default is supplied.

## Behavior

- **New installs / new `settings.json`** → new editor enabled
- **Upgrades from a version before the `useNewEditor` key existed** →
new editor enabled (the key is missing, so the default kicks in)
- **Users who have explicitly toggled the setting** → unchanged (we only
change the default, not stored values)
- The "Go back to classic" button in the KBM settings page is untouched

## Changes

- `src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs` —
`UseNewEditor` property initializer now `true`
- `src/modules/keyboardmanager/dll/dllmain.cpp` — `m_useNewEditor`
member init + `GetNamedBoolean` fallback now `true`; warn-log message
updated to match
-
`src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs`
— both fallback paths in `IsUseNewEditorEnabled` (file missing /
unreadable) now return `true`, so the "Open New Editor" CmdPal command
surfaces by default

## Validation

Built locally on ARM64 Debug (exit code 0):
- `src/modules/keyboardmanager/dll`
- `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys`
- `src/settings-ui/Settings.UI.Library`

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit a33fd3c474)
2026-06-02 15:34:09 +08:00
Niels Laute
c356d65e0d Reword Shortcut Guide module and OOBE descriptions (#48248)
## Summary of the Pull Request

Reword the Shortcut Guide module description and OOBE description so
they describe the feature without referring to `your apps`. The module
description now reads as a single sentence covering Windows and the
active app; the OOBE description is shortened to one paragraph and
refers to `various applications` instead of enumerating the bundled
apps.

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

Two strings in
`src/settings-ui/Settings.UI/Strings/en-us/Resources.resw` changed:

- `ShortcutGuide.ModuleDescription` — now: `Shows an on-screen overlay
of keyboard shortcuts for Windows and the active application.`
- `Oobe_ShortcutGuide.Description` — collapsed to one sentence
describing Windows + various applications, no longer enumerating bundled
apps or mentioning future additions.

No code or behavioral changes.

## Validation Steps Performed

- Built Settings.UI (Debug arm64) – clean.
- Verified the Shortcut Guide module card in Settings UI shows the new
description and the OOBE Shortcut Guide page shows the new paragraph.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
(cherry picked from commit b712fa4d85)
2026-06-02 15:34:09 +08:00
Michael Jolley
2dd1d457eb CmdPal: Synchronize fallback title/subtitle format for consistent scoring (#48085)
## Summary

Fixes #46055 — Standardizes built-in fallback title/subtitle format so
scoring is consistent across all action fallbacks.

## Problem

`MainListPage.cs` scores fallback items by fuzzy-matching the query
against both Title and Subtitle, but with different weights:
- `nameScore = FuzzyScore(query, Title)`
- `descriptionScore = (FuzzyScore(query, Subtitle) - 4) / 2`

Fallbacks that embedded the raw query in Title got artificially higher
scores than those using Subtitle. This made ranking unpredictable.

## Fix

All "action" fallbacks now follow a consistent pattern:
- **Title** = static action description (no query text)
- **Subtitle** = raw query string (unquoted)

"Result" fallbacks (that found a specific matched item) are left
unchanged — they correctly show the matched item name in Title.

## Full Fallback Audit (example query: `notepad`)

| Fallback | Title | Subtitle | Status |
|---|---|---|---|
| WebSearch: Search | "Search the web with Edge" | `Search for notepad`
| **Changed** — was `Search for "notepad"` in subtitle |
| WebSearch: Open URL | "Open in Microsoft Edge" | `Open notepad.com` |
**Changed** — was `Open "notepad.com"` in title |
| Shell: Run | "Run" | `notepad` | Unchanged — already correct |
| Calculator | "3" (result) | `1+2` (query) | Unchanged — intentional
exception |
| Indexer: single result | "notepad.exe" | `C:\Windows\notepad.exe` |
Unchanged — result fallback |
| Indexer: multiple results | "File search" | `Search for notepad in
files` | **Changed** — was `Search for "notepad" in files` in title |
| Windows Settings: single | "Notepad settings" | `Settings > Apps` |
Unchanged — result fallback |
| Windows Settings: multiple | "Search Windows settings..." | `Search
for notepad` | **Changed** — was `Search for "notepad" in Windows
settings` in title |
| Remote Desktop: exact match | "MyPC" (connection) | "Connect to MyPC"
| Unchanged — result fallback |
| Remote Desktop: arbitrary host | "Remote Desktop" | `Connect to
notepad-host` | **Changed** — was `Connect to notepad-host` in title |
| TimeDate | "Monday, May 23" (result) | "Current date" | Unchanged —
result fallback |
| System | "Shut down" (result) | "Shuts down the computer" | Unchanged
— result fallback |
| PowerToys | "Color Picker" (static) | "Pick a color..." | Unchanged —
result fallback |

## Changes

- `FallbackExecuteSearchItem.cs` — Subtitle uses raw query instead of
`"Search for \"{query}\""` format
- `FallbackOpenURLItem.cs` — Title shows browser name (was query),
Subtitle shows raw query (was browser)
- `FallbackOpenFileItem.cs` — Multi-result: Title is static display
name, Subtitle is raw query
- `FallbackWindowsSettingsItem.cs` — Multi-result: Title is static
description, Subtitle is raw query
- `FallbackRemoteDesktopItem.cs` — Arbitrary host: Title is static
"Remote Desktop", Subtitle is raw query

---------

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit b66b044210)
2026-06-02 11:42:13 +08:00
Michael Jolley
bb276d3be9 CmdPal: Fix dock subtitle visibility in compact mode after async update (#48088)
## Summary

When an extension updates its `Subtitle` property asynchronously after
initial render, the `TextVisibilityStates` visual state group
transitions from `TitleOnly` → `TextVisible`. This transition sets
`SubtitleText.Visibility = Visible`, overriding the `CompactStates`
setter that had hidden it.

## Fix

Added `control.UpdateCompactState()` to `OnTextPropertyChanged` in
`DockItemControl.xaml.cs`. This re-applies the compact state after any
text property change. When `IsCompact` is `false`, `UpdateCompactState`
is a no-op — no behavior change for the non-compact path.

Fixes #47980

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 18919eaa40)
2026-06-02 11:42:12 +08:00
Michael Jolley
b7aca7c3fc CmdPal: Reorder dock network stats to match Task Manager order (#48098)
## Summary

Fixes #47939

The Performance Monitor dock band displayed network stats as Receive →
Send, but Task Manager shows Send → Receive. This swaps the order to
match Task Manager.

## Changes

- `PerformanceWidgetsPage.cs`: Swap `_networkUpItem` (Send) before
`_networkDownItem` (Receive) in the band items array.

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit e0854fbaf3)
2026-06-02 11:42:12 +08:00
Michael Jolley
8e3a88df2c CmdPal: Fix hotkey navigation when palette is showing transient dock page (#48089)
## Summary

When a keyboard shortcut opens CmdPal to an extension while the palette
is already showing a dock-launched transient page, `GoHome(false)`
cannot restore the root page — the frame's back stack is empty because
the transient dock page was never pushed on top of root. The user ends
up with only the hotkey-target page in the frame with no way to navigate
back to the main list.

## Root Cause

In `ShellPage.SummonOnUiThread()`, the hotkey-to-page branch called
`GoHome(false)` before sending `ShowWindowMessage`. But when the active
page is a transient dock page, `_currentlyTransient` is still `true` and
the frame back stack is empty, so `GoHome` can't re-establish the root
page as the frame base.

## Fix

Added `ResetToHome()` to `ShellViewModel`, mirroring the pattern already
used in `WindowHiddenMessage` handling:
1. Clears `_currentlyTransient`
2. Calls `_rootPageService.GoHome()` to reset extension state
3. Sends `PerformCommandMessage` for `_rootPage` — navigating
MainListPage into the frame as the base

In `ShellPage.SummonOnUiThread()`, the `GoHome(false)` call in the
`isPage` branch is replaced with `ViewModel.ResetToHome()`. The root
page is then cleanly in the frame before the hotkey target's
`PerformCommandMessage` navigates on top of it.

Fixes #47994

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit ba20da1611)
2026-06-02 11:42:12 +08:00
Niels Laute
fb028e8fdc CmdPal: Reorder Pin to Dock dialog so controls precede the preview (#48250)
## Summary of the Pull Request

Reorder the Pin to Dock dialog content so the configuration controls
(monitor selector, dock section, label options) appear at the top and
the live preview is shown below them. The user now configures the pin
first and sees the resulting preview directly underneath, instead of
staring at the preview and having to scan past it to find the controls.

<img width="515" height="440" alt="image"
src="https://github.com/user-attachments/assets/0d1d0543-2b30-48f5-a1aa-676a165870f5"
/>

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

XAML-only change to
`src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/PinToDockDialogContent.xaml`.
The new visual order inside the `ScrollViewer`/`StackPanel` is:

1. Monitor selector (still `Visibility=""Collapsed""` by default; shown
when more than one monitor is available)
2. Dock section `Segmented` (Start / Center / End)
3. Label options (`Show title` / `Show subtitle` checkboxes)
4. Divider `Rectangle`
5. Preview `Border`

No logic, bindings, `x:Name` identifiers, event handlers, or `x:Uid`
keys are changed.

## Validation Steps Performed

- Built `Microsoft.CmdPal.UI` (Debug arm64) — clean.
- Verified the Pin to Dock dialog renders with controls on top and the
preview underneath; segmented selection, label-option checkboxes, and
the multi-monitor combo still behave as before.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 35f2ed839e)
2026-06-02 11:42:12 +08:00
Michael Jolley
d980697849 CmdPal: Fix GPU index out of range crash in PerfMon widget (#48103)
## Summary

Fixes #47821

The GPU Performance Monitor widget crashes with
`IndexOutOfRangeException` on systems where GPU performance counters
fail to enumerate (common on Intel Arc and hybrid GPU configurations).
The dock band shows `???` and opening the flyout causes an error.

## Root Cause

`GPUStats.CreateGPUImageUrl()` accessed `_stats[index]` without bounds
checking. When `GetGPUPerfCounters()` finds no matching counter
instances, `_stats` remains empty but callers still pass index 0.

`GetGPUName()`, `GetGPUUsage()`, and `GetGPUTemperature()` already have
proper guards (`if (_stats.Count <= index) return ...`) — this fix adds
the same pattern to the one remaining unguarded method.

## Changes

- `GPUStats.cs`: Add bounds check to `CreateGPUImageUrl()` — return
empty string if index out of range

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit e20b5b9c51)
2026-06-02 11:42:12 +08:00
Mike Griese
d42b15f380 cmdpal: fix the dock window border being visible, redux (#48180)
Addenda to #47187

that fix only works if the _hwnd is already set. Actually it's crazy it
ever worked.

Tested by disconnecting and reconnecting RDP a couple times, which
pretty consistently reproduces the problem.

(cherry picked from commit c6a9ad2ad0)
2026-06-02 11:41:59 +08:00
Muyuan Li
b49f722e88 Fix ShortcutGuide v2 crash when Manifests directory is missing (#48171)
## Summary

Fixes #48170 — ShortcutGuide v2 crashes on launch when the bundled
`Manifests` directory is absent from the install path.

### Root Cause

The `Assets\ShortcutGuide\Manifests\*.yml` files were never reaching the
build output directory during the CI solution-level build (`msbuild
PowerToys.slnx /t:Build -graph`). The `CopyToOutputDirectory` metadata
on `<Content>` items does not reliably copy files to a shared
`OutputPath` in this build configuration. As a result, the WiX installer
generator found no yml files to package, and the installed product was
missing the Manifests directory entirely.

At runtime, `PowerToysShortcutsPopulator.Populate()` threw an unhandled
`FileNotFoundException` causing a crash loop.

### Fix (3 layers)

1. **Code resilience** (`Program.cs`, `PowerToysShortcutsPopulator.cs`):
- Wrap `Populate()` in try/catch so a missing manifest degrades
gracefully instead of crashing
   - Add `File.Exists` guard before `File.ReadAllText`

2. **Build output** (`ShortcutGuide.Ui.csproj`):
- Add explicit `CopyManifestsToOutputDir` MSBuild target
(`AfterTargets="Build"`) that copies yml files to
`$(OutDir)Assets\ShortcutGuide\Manifests\` — same pattern as the
existing `CopyPRIFileToOutputDir` target
- Keep `<Content Include>` with `CopyToOutputDirectory` as a fallback
for publish scenarios

3. **Installer packaging** (`generateAllFileComponents.ps1`,
`ShortcutGuide.wxs`):
   - Add `*.yml` to the file inclusion list
- Add `Generate-FileList` / `Generate-FileComponents` calls for
`ShortcutGuideManifestsFiles`
- Add WiX directory definition and `RemoveFolder` component for the
Manifests directory

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit a67fc2d9b7)
2026-06-01 16:32:35 +08:00
Jessica Dene Earley-Cha
c2ea654b3c [CmdPal] Toggle "Show details" / "Hide details" with icon in context menu (#48140)
<!-- 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

Converts the "Show details" context menu command into a toggle that
switches between "Show details" and "Hide details" with appropriate
icons, and fixes the icon not rendering in the context menu.

Address internal a11y bug.

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

(cherry picked from commit c78f6e52a0)
2026-06-01 16:32:35 +08:00
thetsaw
80e0a4d09e Add DiskAnalyzer to third-party Run plugins list (#48106)
## Summary of the Pull Request

Adds **DiskAnalyzer** to the General plugins table in
`doc/thirdPartyRunPlugins.md`.

- **Plugin:**
[Community.PowerToys.Run.Plugin.DiskAnalyzer](https://github.com/thetsaw/PowerToys.Plugin)
- - **Author:** thetsaw
- - **Keyword:** `ds`
- - **License:** MIT
- - **Platforms:** x64 and ARM64
### What it does
Scan any folder or drive to find the largest files and subfolders, view
drive usage with visual progress bars, and navigate your filesystem all
from PowerToys Run.

## PR Checklist

- [x] Plugin has been publicly available
- [ ] - [x] MIT licensed
- [ ] - [x] Releases include x64 and ARM64 zips
- [ ] - [x] plugin.json is correctly formatted
- [ ] - [x] README includes install instructions
## Detailed Description

This is a documentation-only change adding one row to the third-party
plugins table. No source code, binaries, or build files are modified.

(cherry picked from commit 9a55209d13)
2026-05-29 12:49:14 +08:00
Copilot
8c93854bbb Rename Settings UI label from “Shortcut Guide V2” to “Shortcut Guide” (#48151)
## Summary of the Pull Request

This updates PowerToys Settings to remove the obsolete “V2” suffix from
the Shortcut Guide module name. The UI now consistently shows **Shortcut
Guide**.

## PR Checklist

- [ ] **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
- [ ] **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

- **Settings navigation label**
  - Updated `Shell_ShortcutGuide.Content` to `Shortcut Guide`.
- **Module title**
  - Updated `ShortcutGuide.ModuleTitle` to `Shortcut Guide`.
- **OOBE title**
  - Updated `Oobe_ShortcutGuide.Title` to `Shortcut Guide`.

```xml
<data name="Shell_ShortcutGuide.Content" xml:space="preserve">
  <value>Shortcut Guide</value>
</data>
```

## Validation Steps Performed

- N/A for behavior-level validation in this description (change is
limited to localized display strings).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
(cherry picked from commit 0cb6fe250b)
2026-05-29 12:49:14 +08:00
moooyo
1aba1e170c Remove "NEW" tag from Power Display and Grab and Move (#48174)
## Summary of the Pull Request
Both Power Display and Grab and Move have matured beyond their initial
release phase. This removes the "NEW" `InfoBadge` from their navigation
items in Settings, the two parent navigation groups (Windowing &
Layouts, Input / Output) that surfaced the badge when collapsed, and
clears the `IsNew` flag for Power Display in the OOBE shell.

<img width="1867" height="973" alt="image"
src="https://github.com/user-attachments/assets/533f271c-c70f-414f-a76a-43fd9ffbbd44"
/>
<img width="497" height="575" alt="image"
src="https://github.com/user-attachments/assets/fe1e97c3-c806-4f42-a836-76e042630d61"
/>
<img width="1619" height="1027" alt="image"
src="https://github.com/user-attachments/assets/f5db715b-bc69-4505-803a-18a9b2716280"
/>

## PR Checklist

- [x] Closes: #48153
- [x] **Communication:** Tracked by the linked issue
- [x] **Tests:** Markup-only change; Settings.UI builds clean with WinUI
markup compiler (no XAML errors)
- [x] **Localization:** No end-user-facing strings changed
- [ ] **Dev docs:** N/A
- [ ] **New binaries:** N/A
- [ ] **Documentation updated:** N/A

## Detailed Description of the Pull Request / Additional comments
Files touched:
- `src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml` —
removed four `<InfoBadge Style="{StaticResource NewInfoBadge}" />`
blocks on `GrabAndMoveNavigationItem`, `PowerDisplayNavigationItem`, and
on the two parent group headers `WindowingAndLayoutsNavigationItem` and
`InputOutputNavigationItem` (the parent badges existed only to surface a
NEW child when the group was collapsed; with no NEW children left in
those groups, the parent badges are now stale).
- `src/settings-ui/Settings.UI/OOBE/ViewModel/OobeShellViewModel.cs` —
flipped `(PowerToysModules.PowerDisplay, true)` to
`(PowerToysModules.PowerDisplay, false)`. Grab and Move was already
`false`.

No other modules or strings affected.

## Validation Steps Performed
- Built `src\settings-ui\Settings.UI\PowerToys.Settings.csproj`
(Release|x64) with MSBuild from VS 18 Enterprise;
`PowerToys.Settings.dll` produced with 0 errors related to this change.
WinUI markup compiler would have aborted before producing the DLL if the
XAML had syntax issues.
- Diff inspected: only the five intended deletions/edits, no collateral
changes.
- Visual run-time verification of the Settings navigation pane is
recommended before merge.

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit c0cb9417ad)
2026-05-29 12:49:14 +08:00
moooyo
c5a18aa488 [PowerDisplay] Fix false-positive crash detection on cooperative shutdown (#48173)
<!-- 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

Cooperative shutdowns of `PowerDisplay.exe` — Runner's `TerminateApp`
NamedPipe message, the `Terminate` named event, tray-quit, Runner-exit
detection, and PowerToys upgrades — all call `Environment.Exit(0)`
immediately. If DDC/CI discovery is mid-flight, that path skips the
`try/finally` that owns `CrashDetectionScope`, leaving `discovery.lock`
on disk. Phase 0 at the next `PowerDisplay.exe` startup then treats this
orphan as evidence of a real crash and auto-disables the module,
surfacing the "PowerDisplay has crashed" InfoBar in Settings UI.

This PR adds an `AppDomain.ProcessExit` safety-net inside
`CrashDetectionScope`. ProcessExit fires for `Environment.Exit` but
**not** for `FailFast` / BSOD / external `TerminateProcess` — exactly
the partition we need: cooperative exit → best-effort delete the lock;
involuntary kill → leave the lock for Phase 0 to detect (original design
intent preserved).

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

- [x] Closes: #48169
- [x] **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
<!-- no user-facing strings changed -->
- [x] **Dev docs:** Added/updated <!-- inline XML doc on
CrashDetectionScope explains the ProcessExit partition -->
- [ ] **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

### Root cause

`CrashDetectionScope.Begin()` writes `discovery.lock` before DDC/CI
capability fetch and `Dispose()` deletes it when the `using` block
exits. The lock is intentionally designed to survive any code path that
cannot run user-mode cleanup (BSOD, kernel OOM, `TerminateProcess`), so
that the next `PowerDisplay.exe` start can see it and run Phase 0 (write
`crash_detected.flag`, set `enabled.PowerDisplay=false` in global
`settings.json`, signal `AutoDisablePowerDisplayEvent`).

The bug is that several **cooperative** shutdown paths route to
`Environment.Exit(0)` immediately:

| Path | Code |
|---|---|
| Runner's `TerminateApp` NamedPipe | `App.xaml.cs::OnNamedPipeMessage`
→ `Shutdown()` → `Environment.Exit(0)` |
| `Terminate` named event | `App.xaml.cs::OnLaunched` →
`RegisterEvent(..., () => Environment.Exit(0), "Terminate")` |
| Tray-quit | `TrayIconService` callback → `Environment.Exit(0)` |
| Runner-exit detection | `RunnerHelper.WaitForPowerToysRunner` callback
→ `Environment.Exit(0)` |

`Environment.Exit` calls `ExitProcess` under the hood, which terminates
all threads abruptly. Background `Task.WhenAll` doing DDC capability
fetch is killed mid-flight; the `finally` block that calls
`scope.Dispose()` never runs; `discovery.lock` orphans; Phase 0 next
time false-positives.

Concrete repro from logs:
- `15:08:42.510` lock written
- `15:08:42.79` probe monitor #1
- `15:08:46.92` probe monitor #2 (started, not finished — typical probe
takes ~5s)
- `15:08:49.03` `TerminateApp` received → `Environment.Exit(0)` → no
`Dispose` log line
- `15:10:10.03` next startup: Phase 0 sees orphan lock with `pid:17712,
startedAt:2026-05-28T07:08:42Z` → writes `crash_detected.flag` →
auto-disables

### Fix

`CrashDetectionScope.Begin()` now also subscribes to
`AppDomain.CurrentDomain.ProcessExit`. The handler does a best-effort
`File.Delete(_lockPath)` (swallowing exceptions, as required for
ProcessExit handlers). `Dispose()` unsubscribes before deleting. An
`Interlocked.Exchange` guards the race between Dispose and ProcessExit
so only one of the two performs the delete.

ProcessExit's semantics match the cooperative/involuntary partition
exactly:

| Shutdown path | ProcessExit fires? | Behavior after this PR |
|---|---|---|
| `Environment.Exit(code)` (all 4 paths above) | yes | lock deleted by
handler |
| `Environment.FailFast` | no | lock survives → Phase 0 catches it
(correct: explicit FailFast = real failure) |
| BSOD / external `TerminateProcess` / kernel OOM | no | lock survives →
Phase 0 catches it (correct: original design) |
| Discovery completes normally / throws | n/a | `try/finally` calls
`Dispose()` as before; handler unsubscribed first |

### Testability

A new `IProcessExitHook` interface abstracts the subscription so unit
tests can simulate ProcessExit without terminating the test runner.
Production code uses the default `AppDomainProcessExitHook` singleton;
tests inject a fake whose `RaiseExit()` invokes subscribed handlers
synchronously.

### Files touched

-
`src/modules/powerdisplay/PowerDisplay.Lib/Services/IProcessExitHook.cs`
*(new)* — interface + production singleton
-
`src/modules/powerdisplay/PowerDisplay.Lib/Services/CrashDetectionScope.cs`
— subscribe in `Begin`, unsubscribe in `Dispose`, add `OnProcessExit`
handler, expanded class doc
-
`src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/CrashDetectionScopeTests.cs`
*(new)* — 10 unit tests

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

### Automated

10 new unit tests in `CrashDetectionScopeTests`, all passing:

```
Passed Begin_WritesLockFileAtomically
Passed Begin_SubscribesToProcessExit
Passed Dispose_UnsubscribesFromProcessExit
Passed Dispose_DeletesLockFile
Passed ProcessExitFired_BeforeDispose_DeletesLock      (core scenario)
Passed ProcessExitFired_AfterDispose_DoesNothing
Passed Dispose_AfterProcessExit_DoesNotThrow
Passed ProcessExitFired_LockFileMissing_DoesNotThrow
Passed Dispose_IsIdempotent
Passed MultipleScopes_DoNotShareState
```

Full `PowerDisplay.Lib.UnitTests` suite: **129 / 132 passing**. The 3
failures (`DetectOrphanAndDisable_RunsFullSequenceWhenOrphanPresent`,
`DetectOrphanAndDisable_HandlesUnknownVersionAsOrphan`,
`DetectOrphanAndDisable_LeavesLockIntactOnSignalFailure`) are
**pre-existing on `main`** — they fail with `REGDB_E_CLASSNOTREG` from
`Constants.AutoDisablePowerDisplayEvent()` (WinRT activation factory not
COM-registered in the test environment). Verified by stashing this PR's
changes and re-running the same 3 tests on baseline `main` — same
failures, same cause, unrelated to this change.

### Manual

1. Reproduced the original false-positive on `main`:
- Enable PowerDisplay → open Settings UI → quickly toggle PowerDisplay
off
- Observe `discovery.lock` left in
`%LOCALAPPDATA%\Microsoft\PowerToys\PowerDisplay\`
- Re-enable PowerDisplay → Phase 0 writes `crash_detected.flag` →
InfoBar appears
2. Repeated the same steps with this branch:
- Toggling PowerDisplay off cleanly deletes `discovery.lock`
(ProcessExit handler ran)
- Re-enabling PowerDisplay shows no InfoBar, no `crash_detected.flag`
created
3. BSOD path is unchanged (verified by inspecting the conditional logic
— `AppDomain.ProcessExit` does not fire for involuntary terminations;
the lock survives just as before).

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit cd5027fa1a)
2026-05-29 12:49:14 +08:00
Copilot
df019c09e6 Rename OOBE overview Learn link label to “Documentation” (#48155)
## Summary of the Pull Request

Renames the OOBE welcome/overview hyperlink label from **“Documentation
on Microsoft Learn”** to **“Documentation”** for brevity and
consistency.
Scope is limited to the localized string resource used by the OOBE
overview page.

## PR Checklist

- [ ] **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
- [ ] **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

- **Resource update (OOBE Overview)**
- Updated `Oobe_Overview_DescriptionLinkText.Text` in
`src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`.

```xml
<data name="Oobe_Overview_DescriptionLinkText.Text" xml:space="preserve">
  <value>Documentation</value>
</data>
```

## Validation Steps Performed

- Confirmed the OOBE overview string key now resolves to
**“Documentation”**.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
(cherry picked from commit 7da62cdb0a)
2026-05-29 12:49:14 +08:00
Eymard Silva
d97301f06f Handle complex calculator results (#47506)
## Summary of the Pull Request

Return a friendly calculator error when Mages evaluates an expression to
a complex number instead of letting decimal conversion throw.

This fixes the PowerToys Run Calculator result for expressions such as
`sqrt(-1)` by detecting `System.Numerics.Complex` results before decimal
conversion and showing a localized error message instead.

Fixes #43937

## PR Checklist

- [x] Closes: #43937
- [ ] **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
- [x] **Dev docs:** Added/updated
- [x] **New binaries:** Added on the required places
- [x] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [x] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [x] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [x] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [x] **Documentation updated:** Not required for this bug fix.

## Detailed Description of the Pull Request / Additional comments

The Calculator plugin previously passed complex results from Mages into
`Convert.ToDecimal`, which caused an exception for expressions like
`sqrt(-1)`.

This PR updates the calculator result transformation logic to detect
`System.Numerics.Complex` and return a localized user-facing error
message: `Complex numbers are not supported`.

It also updates calculator query tests to cover both direct keyword and
global query behavior.

## Validation Steps Performed

- Added unit test coverage for `=sqrt(-1)` returning `Complex numbers
are not supported`.
- Added unit test coverage for global query `sqrt(-1)` returning no
result instead of surfacing an unhandled exception.
- Ran `git diff --check`.
- Attempted local build/test with the PowerToys build scripts, but local
validation was blocked by Visual Studio/VC tooling configuration issues
unrelated to this change: `PlatformToolsetVersion` resolves to an empty
value during restore/build.

(cherry picked from commit c46083dd8d)
2026-05-29 12:49:13 +08:00
Mike Griese
86ca7c5661 Move CmdPal API spec back to cmdpal/ directory (#48160)
Reverts 0819a62 / #46926

The cmdpal API is literally generated from this spec document. It needs
to live with the rest of the code to work correctly.

Docs for authoring cmdpal extensions are on
https://learn.microsoft.com/en-us/windows/powertoys/command-palette/extension-development,
and we should direct docs commentary there.

(cherry picked from commit 65112a7b05)
2026-05-29 12:49:13 +08:00
🄂ʏᴇᴅ 🄰ʙᴅᴜʟ 🄰ᴍᴀ🄝 ✧
e37d1d5d04 Fix project template settings heading (#48148)
## Summary
- Fix a grammar typo in the PowerToy project template README heading.
- Change "Settings Informations" to "Settings Information".

## Validation
- Ran `git diff --check`.

(cherry picked from commit 6be6509c46)
2026-05-29 12:48:44 +08:00
Boliang Zhang (from Dev Box)
ac6aa80401 Merge main into stable for 0.100 release (rev 6) 2026-05-26 15:19:20 +08:00
Boliang Zhang (from Dev Box)
c4a83be733 Merge main into stable for 0.100 release (rev 5) 2026-05-26 11:28:54 +08:00
Boliang Zhang (from Dev Box)
34d01e998c Merge main into stable for 0.100 release (rev 4) 2026-05-25 13:03:13 +08:00
Boliang Zhang (from Dev Box)
b1adc8548f Merge main into stable for 0.100 release (rev 3) 2026-05-23 00:06:56 +08:00
Boliang Zhang (from Dev Box)
0e71f616de Merge main into stable for 0.100 release (rev 2) 2026-05-22 14:04:49 +08:00
Boliang Zhang (from Dev Box)
4f0f614f67 Merge main into stable for 0.100 release 2026-05-21 13:58:32 +08:00
Boliang Zhang (from Dev Box)
ddf9815613 Merge remote-tracking branch 'origin/main' into stable 2026-05-19 10:55:40 +08:00
Boliang Zhang (from Dev Box)
184ccb75ec Merge origin/main into stable 2026-04-29 13:38:21 +08:00
Boliang Zhang (from Dev Box)
455dc52430 Merge origin/main into stable 2026-04-26 21:05:06 +08:00
Boliang Zhang
ed1d15f9a1 Fix: Install CommandPalette.Extensions.winmd to WinUI3Apps for COM marshalling (#47210)
## Summary

Follow-up fix for #47177. Installs `CommandPalette.Extensions.winmd` to
the `WinUI3Apps\` directory (ExternalLocation) instead of the root
install folder.

## Problem

PR #47177 moved the sparse package's `ExternalLocation` from root to
`WinUI3Apps\`, but the `CommandPalette.Extensions.winmd` was still
installed to root via `DirectoryRef=INSTALLFOLDER`. The WinRT runtime
needs this winmd in the ExternalLocation directory for COM proxy/stub
creation during cross-process CmdPal extension activation. Without it,
`CoCreateInstance` returns `E_NOINTERFACE` and the PowerToys extension
fails to load in Command Palette.

## Fix

Split the `BaseApplications.wxs` `DirectoryRef` into two blocks:
- **winmd component** -> `WinUI3AppsInstallFolder` (for WinRT COM
marshalling)
- **auto-generated components** -> `INSTALLFOLDER` (unchanged, avoids
ICE30 conflict with `WinUI3ApplicationsFiles`)

## Validation

- [x] Local WiX compilation: no ICE30 errors
- [x] ADO CI build 145429943 (v0.99.1): passed
- [x] Manual verification on 25H2: CmdPal loads 55 commands after winmd
placed in WinUI3Apps

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-25 23:48:22 +08:00
Boliang Zhang (from Dev Box)
31da4fa112 Merge origin/main into stable 2026-04-24 22:38:59 +08:00
Boliang Zhang (from Dev Box)
3ee7518850 Merge origin/main into stable 2026-04-24 22:34:19 +08:00
Boliang Zhang (from Dev Box)
1bfa42b270 Merge origin/main into stable 2026-04-24 14:15:50 +08:00
Boliang Zhang (from Dev Box)
e0979fce59 Merge origin/main into stable 2026-04-24 10:59:26 +08:00
Boliang Zhang (from Dev Box)
b2684f74f5 Merge origin/main into stable 2026-04-24 10:55:14 +08:00
Muyuan Li
b67e11d000 Fix CmdPal crash when typing in search box (#47148)
Add reentrancy guard for FilteredItems ObservableCollection mutations.

WinUI3's native XAML renderer can pump the message loop while processing
a CollectionChanged notification from InPlaceUpdateList. This allows a
second DoOnUiThread task to begin mutating FilteredItems while the first
is still mid-update, causing heap corruption and an access violation
(0xc0000005) in ntdll.dll.

The fix introduces RunFilteredItemsUpdate() which uses a boolean flag to
detect same-thread reentrancy (C# lock is reentrant so _listLock cannot
prevent this). When reentrancy is detected, only the latest pending
update is stored and executed after the in-flight mutation completes,
ensuring the UI converges to the newest state without overlapping
mutations.

Fixes: 100% reproducible crash in CmdPal when typing any character in
the search box (build ID 145015494).

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

- [x] Closes: #47145
<!-- - [ ] 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 <223556219+Copilot@users.noreply.github.com>
2026-04-23 17:19:11 +08:00
Boliang Zhang (from Dev Box)
a5bc91028f Merge remote-tracking branch 'origin/main' into stable 2026-04-23 16:18:06 +08:00
Boliang Zhang (from Dev Box)
1eebd47af0 Merge remote-tracking branch 'origin/main' into stable 2026-04-22 17:18:11 +08:00
Boliang Zhang (from Dev Box)
1f840000f5 Merge remote-tracking branch 'origin/main' into stable 2026-04-21 14:29:24 +08:00
Boliang Zhang (from Dev Box)
04fe688e7a Merge remote-tracking branch 'origin/main' into stable 2026-04-20 16:34:06 +08:00
Boliang Zhang (from Dev Box)
8fc31de489 Merge main into stable for 0.99 release 2026-04-16 15:36:07 +08:00
Boliang Zhang (from Dev Box)
0a510119c8 Remove duplicate PowerDisplay model classes after merge from main
The merge from main added a PowerDisplay.Models project reference to
Settings.UI.Library, but the old duplicate model classes (CustomVcpValueMapping
and ColorPresetItem) in Settings.UI.Library/Models/ were not removed, causing
CS0104 ambiguous reference errors. Additionally, two files had stale using
aliases and a XAML DataTemplate referenced the old namespace.

Changes:
- Delete Settings.UI.Library/Models/CustomVcpValueMapping.cs (duplicate)
- Delete Settings.UI.Library/Models/ColorPresetItem.cs (duplicate)
- Update PowerDisplayPage.xaml.cs and PowerDisplayViewModel.cs using directives
- Update PowerDisplayPage.xaml x:DataType to use pdmodels namespace

Verified with local build of both Settings.UI and PowerDisplay projects.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-14 11:08:53 +08:00
Boliang Zhang (from Dev Box)
38dfc55a2d Merge branch 'main' into stable
# Conflicts:
#	src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs
#	src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs
#	src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs
#	src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs
#	src/modules/keyboardmanager/common/Helpers.cpp
#	src/settings-ui/Settings.UI.Library/MonitorInfo.cs
#	src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs
#	src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj
#	src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs
#	src/settings-ui/Settings.UI/SettingsXAML/Views/CustomVcpMappingEditorDialog.xaml.cs
2026-04-13 16:33:34 +08:00
Niels Laute
89b2dfe9ac [KBM] Manual key selection — code review fixes (#46377)
Addresses code review feedback on the KBM manual key selection feature.
No new user-facing behavior; all changes are correctness, robustness,
and maintainability fixes.

## Summary of the Pull Request

- **Localization:** All hard-coded `RemappingDialog.Title` assignments
replaced with `ResourceHelper.GetString()`; added
`RemappingDialog_TitleEdit` resource key
- **VK_DISABLED centralization:** Replaced scattered `0x100`/`"256"`
literals and local `const string vkDisabledCode` with `private const int
VkDisabled = 0x100` / `private const string VkDisabledString = "256"` on
`MainPage`
- **Disable action validation:** Added
`ValidationHelper.ValidateDisableMapping()` — same trigger-key rules as
other action types (empty keys, modifier-only, illegal shortcuts,
duplicates, conflicting modifier variants); wired into
`ValidateMapping()` switch
- **Binding-safe dropdown revert:** `TriggerKeyDropDown_KeyChanged` /
`ActionKeyDropDown_KeyChanged` no longer set `dropDown.KeyName =
e.OldKeyName` on failure (breaks `{Binding}` expression); now use
`RevertKeySelection(keys, index)` which does
`ObservableCollection.RemoveAt` + `Insert` to force a binding-tracked
refresh without touching the DP directly. `NewKeyCode == 0` ("None") is
rejected via the same path
- **Dropdown validation:** `ValidateDropDownSelection` skips
`string.IsNullOrEmpty` placeholder slots (added by
`HandleAutoGrowShrink`) when checking repeated-modifier and max-size
rules
- **`SetActionType`:** Replaced hard-coded `SelectedIndex` with
tag-matching iteration over `ActionTypeComboBox.Items`; immune to XAML
item reorder
- **`ServiceStatusHelper`:** Dispose all `Process` objects returned by
`GetProcessesByName` before returning; prevents handle accumulation on
the 3-second polling timer
- **`KeyDropDownButton.GetKeyList()`:** Filter out `KeyCode == 0`
entries (native "None" sentinel for shortcut lists) before caching
- **`SettingsManager`:** `_mappingService!` used consistently in
`CreateSettingsFromKeyboardManagerService`
- **`KeyboardHookHelper`:** Constructor catch broadened from
`DllNotFoundException or InvalidOperationException` to `Exception`

## PR Checklist

- [ ] **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
- [ ] **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 `RevertKeySelection` pattern: in WinUI, assigning directly to a
bound `DependencyProperty` overwrites the binding expression. Using
`ObservableCollection.RemoveAt` + `Insert` instead raises
`CollectionChanged(Replace)`, causing the binding to re-read from the
source without clearing the expression.

```csharp
private static void RevertKeySelection(ObservableCollection<string> keys, int index)
{
    string current = keys[index];
    keys.RemoveAt(index);
    keys.Insert(index, current);
}
```

## Validation Steps Performed

Manually verified: dropdown key selection and revert on invalid
selection, Disable mapping save/load with the new validation, "None"
absent from key picker flyout, `SetActionType` correctly selects items
with preserved XAML order.

---------

Co-authored-by: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-04-09 13:10:05 -04:00
Jaylyn Barbee
2ef65e7d63 [KBM] Fixes to text replacement issues (#46794)
<!-- 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 attempts to fix some of the issues that were introduced in
0.98.0 with text replacement

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

- [x] Closes: #46498
- [x] Closes: #46440
- [x] Closes: #46366

<!-- 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
In 0.98.0 I made a change to support multiline text replacement using
Ctrl + V. This was very inconsistent so I have reverted back to
_basically_ the same approach we were using before except now when we
encounter a newline indicator we send "Shift + Enter" so that this works
in chat boxes and plan editors.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manual testing with single line and multiline replacements in Teams,
Terminal, Git bash, Edge, etc
2026-04-09 13:10:05 -04:00
Zach Teutsch
3dc5b3a3ed [Keyboard Manager] Remove service enable/disable separate from module, fix editor clear shortcut (#46530)
Two changes to shortcuts here:
1) Remove toggling the KBM service with a shortcut or via command
palette
2) Ensure that shortcut is disabled for editor when shortcut is cleared
2026-03-26 13:35:11 -04:00
Zach Teutsch
fc5b65c5c3 [Keyboard Manager] Allow whitespace-only TextRemappings (#46510)
Title.

Closes #46453
2026-03-26 13:35:11 -04:00
Zach Teutsch
da5448b169 fix merge mixup for hotfix 0.98.1 2026-03-24 22:17:10 -04:00
Zach Teutsch
1ab685ee07 Merge 0.98.1 hotfixes into stable
Cherry-picked commits:
- Make KBM Editor pinnable (#46482)
- CmdPal: Fix missing primary context command for late-bound items (#46131)
- CmdPal: Ensure DockWindow property cleans up after itself (#46303)
- CmdPal: Hotfix commonCallbacks array initial count to prevent negative number (#46215)
- CmdPal: Fix missing app context menu actions on the main page (#46293)
- CmdPal: Fix dock popup XamlRoot handling on DockControl (#46305)
- CmdPal: Reduce DockWindow backdrop switching and visual artifacts (#46309)
- Always On Top: The opacity should be able to configure the hotkey individually (#46410)
- [OOBE] Ensure the Settings button on the SCOOBE page opens Home, not a blank page (#46203)
- CmdPal: Fix scroller scrolling and down glyph (#46447)
- [Settings] Decouple Settings.UI.Library from PowerDisplay.Lib to fix (#46325)
2026-03-24 21:25:13 -04:00
Zach Teutsch
cf137ccbbc Merge branch 'main' into stable 2026-03-16 21:39:04 -04:00
Zach Teutsch
2cc051ee33 Merge branch 'main' into stable 2026-03-12 14:29:12 -04:00
Zach Teutsch
efc89b01ff Merge branch 'main' into stable 2026-03-10 22:42:57 -04:00
Zach Teutsch
9717eaac4c Merge branch 'main' into stable 2026-03-09 22:19:50 -04:00
Zach Teutsch
b5716d0499 add kbm ui dll to esrp signing json 2026-03-04 21:58:03 -05:00
Zach Teutsch
a4d23b7607 fix ESRP path for kbm winui 2026-03-04 20:24:20 -05:00
Zach Teutsch
5409b1b907 fix AppListItem.cs dupe function from merge 2026-03-04 18:12:01 -05:00
Zach Teutsch
fbe952715c Merge branch 'main' into stable 2026-03-04 17:05:50 -05:00
Shawn Yuan
034759f949 Fix WinuiEx crash issue (#45443)
<!-- 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 crash related to `IsShownInSwitchers` when explorer.exe is not
running. The property has been removed from XAML and is now set in the
C# backend with added exception handling to improve stability. No
changes were made for projects where the property is set to true, as
they are not affected.

<!-- 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
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [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: vanzue <vanzue@outlook.com>
2026-02-06 17:31:07 +08:00
Thanh Nguyen
934c3bbce9 Fix CursorWrap "Automatically activate on utility startup" setting not persisting (#45210)
## Summary of the Pull Request

Fixes #45185 - CursorWrap "Automatically activate on utility startup"
setting cannot be disabled, and prevents spurious activation on startup.

## PR Checklist

- [x] Closes: #45185
- [x] **Communication:** Issue was reported by community; fix follows
established patterns from MousePointerCrosshairs
- [x] **Tests:** Manual validation performed by contributor (video
available)
- [x] **Localization:** No new user-facing strings added
- [ ] **Dev docs:** N/A - bug fix only
- [ ] **New binaries:** N/A - no new binaries
- [ ] **Documentation updated:** N/A - bug fix only

## Detailed Description of the Pull Request / Additional comments

### Problem

Users reported that disabling the "Automatically activate on utility
startup" setting for CursorWrap does not work - the mouse hook always
starts automatically regardless of the setting value.

### Root Causes

1. **`dllmain.cpp` `enable()` method**: `StartMouseHook()` was always
called unconditionally, ignoring `m_autoActivate`.
2. **`MouseUtilsViewModel.cs` `IsCursorWrapEnabled` setter**: enabling
CursorWrap forced `AutoActivate = true`, overriding the user's
preference.
3. **Startup edge case**: the trigger event could remain signaled from a
previous session, immediately toggling CursorWrap on startup even when
AutoActivate is off.

### Solution

1. **`src/modules/MouseUtils/CursorWrap/dllmain.cpp`**: only start the
mouse hook if `m_autoActivate` is true.
2. **`src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs`**:
remove the line that forced `AutoActivate = true` when enabling
CursorWrap.
3. **`src/modules/MouseUtils/CursorWrap/dllmain.cpp`**: reset the
trigger event on enable to avoid immediate activation on startup.

### Pattern Reference

This fix follows the same pattern used by **MousePointerCrosshairs**
module which has a similar `AutoActivate` setting that works correctly.

## Validation Steps Performed

### Build

- `tools\build\build.ps1 -Platform x64 -Configuration Debug`

### Manual validation (contributor)

#### Test Case 1: AutoActivate = false (should NOT auto-start mouse
hook)

1. Open PowerToys Settings → Mouse Utilities → Cursor Wrap
2. Enable Cursor Wrap
3. **Disable** "Automatically activate on utility startup"
4. Close PowerToys completely (right-click tray icon → Exit)
5. Restart PowerToys
6. **Expected Result**: CursorWrap module is loaded but mouse hook is
NOT active - cursor does NOT wrap at screen edges
7. Press activation hotkey (default: `Win+Alt+U`)
8. **Expected Result**: Mouse hook activates, cursor now wraps at screen
edges
9. **Actual Result**:  Works as expected

#### Test Case 2: AutoActivate = true (should auto-start mouse hook)

1. Open PowerToys Settings → Mouse Utilities → Cursor Wrap
2. Enable Cursor Wrap
3. **Enable** "Automatically activate on utility startup"
4. Close PowerToys completely
5. Restart PowerToys
6. **Expected Result**: Mouse hook is immediately active, cursor wraps
at screen edges without pressing hotkey
7. **Actual Result**:  Works as expected

#### Test Case 3: Setting persistence after restart

1. Set AutoActivate = false, restart PowerToys
2. Open Settings and verify AutoActivate is still false
3. Set AutoActivate = true, restart PowerToys
4. Open Settings and verify AutoActivate is still true
5. **Actual Result**:  Setting persists correctly

#### Test Case 4: Hotkey toggle works correctly

1. With AutoActivate = false, restart PowerToys
2. Press hotkey → cursor should start wrapping
3. Press hotkey again → cursor should stop wrapping
4. **Actual Result**:  Hotkey toggle works correctly

---

**Note**: Video demonstration available from contributor.
2026-02-05 20:34:15 +08:00
Mike Hall
ac548297c9 Add option to disable CursorWrap when on a single monitor. (#45303)
## Summary of the Pull Request
CursorWrap wraps on the outer edge of monitors, if a user is swapping
between a laptop and docked laptop with external monitors the user might
want to only enable wrapping when connected to external monitors, and
disable when only on the laptop.

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

- [ ] Closes: #45198
- [ ] Closes: #45154
- [ ] **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
Currently CursorWrap will wrap around the horizontal/vertical edges of
monitors, if the user has more than one monitor the outer edges are used
as wrap targets, if the user only has one monitor (perhaps a laptop)
wrapping might be temporarily disabled until additional external
monitors are added (such as being plugged into a dock or using a USB-C
monitor).

The new option will disable wrapping if only a single monitor is
detected, monitor detection is dynamic.

## Validation Steps Performed
Validated on a Surface Laptop 7 Pro (Intel) with a USB-C External
Monitor.

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-02-05 20:33:49 +08:00
Shawn Yuan
17a215d321 Fix Advanced Paste settings page crash issue (#45207)
<!-- 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 pull request refactors the `AdvancedPasteAdditionalActions` class
to use private backing fields and custom property accessors for its
action properties. This change allows for better control over property
initialization and ensures that the properties always have valid,
non-null default values.

**Refactoring for property initialization and null safety:**

* Introduced private backing fields (`_imageToText`, `_pasteAsFile`,
`_transcode`) for the `ImageToText`, `PasteAsFile`, and `Transcode`
properties in `AdvancedPasteAdditionalActions`, replacing
auto-properties.
* Updated the property accessors for `ImageToText`, `PasteAsFile`, and
`Transcode` to use the new backing fields and ensure that a new default
instance is assigned if a null value is provided during initialization.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #45189
<!-- - [ ] 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-02-05 10:37:31 +08:00
Jaylyn Barbee
1b6a8c54ff [Light Switch] Fix Light Switch start up logic (#45304)
<!-- 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
Title

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

- [x] Closes: https://github.com/microsoft/PowerToys/issues/45291
<!-- - [ ] 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

<!-- 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
Before, there was a function that initialized some variables about the
current system state that were later used to check against if that state
needed to change in a different function. That caused from some issues
because I was reusing the function for a double purpose. Now the
`SyncInitialThemeState()` function in the State Manager will sync those
initial variables and apply the correct theme if needed.

I also removed an unnecessary parameter from `onTick`

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manual testing
2026-02-05 10:29:42 +08:00
Kai Tao
67518dd754 Workspace: Fix an overlay issue for workspace snapshot draw (#45183)
<!-- 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
Root cause: Workspaces uses DPI-unaware coordinates (via
GetDpiUnawareScreens()
which runs in a temporary DPI-unaware thread) to store/match window
positions
across different DPI settings. However, WorkspacesEditor itself uses
PerMonitorV2
DPI awareness for UI clarity. When assigning these DPI-unaware
coordinates directly
to WPF window properties, WPF automatically scaled them again based on
current DPI,
causing incorrect overlay positioning.

Fix: Use SetWindowPositionDpiUnaware() to bypass WPF's automatic DPI
scaling
by temporarily switching to DPI-unaware context when calling Win32
SetWindowPos.

Fix #45174

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

- [ ] Closes: #45174
<!-- - [ ] 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 in local build vs production build, and the problem fixed in
local build.
2026-02-05 10:29:36 +08:00
Kai Tao
18d1fd568c PowerToys extension: Bundle localization files into installer (#45194)
<!-- 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: #45171
<!-- - [ ] 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="925" height="612" alt="image"
src="https://github.com/user-attachments/assets/214ead95-504a-4e48-bc25-138323d973f9"
/>
2026-02-05 10:29:31 +08:00
leileizhang
3eae35f356 [ImageResizer] Fix Image Resizer not working after upgrade on Windows 10 (#45184)
<!-- 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 an issue where Image Resizer stops working after upgrading
PowerToys on Windows 10
- Root cause: the PackageIdentityMSIX (sparse app) was not being
properly cleaned up during upgrade

## Problem
Previous versions of PowerToys installed the sparse app on Windows 10.
The current version only installs it on Windows 11+ (build >= 22000).
During upgrade on Windows 10:
1. The `NOT UPGRADINGPRODUCTCODE` condition prevented the uninstall
action from running
2. The Windows 11 version check prevented the new sparse app from being
installed
3. Result: the old sparse app remained on the system, causing Image
Resizer to malfunction

## Fix
Changed the `UninstallPackageIdentityMSIX` condition from:
Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")
to:
Installed AND (REMOVE="ALL")

This ensures the old sparse app is properly cleaned up during upgrades,
which is also consistent with other similar cleanup

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

- [x] Closes: #45178 #45280
<!-- - [ ] 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
1. Install PowerToys version 0.96.1 on Windows 10.
2. Upgrade to version 0.97.1.
3. Run Get-AppxPackage -Name "*Sparse*" in PowerShell to check whether a
Sparse App package is present.
2026-02-05 10:29:25 +08:00
Niels Laute
e349779766 Fix contrast issue (#45367)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

- [X] Closes: #42261
<!-- - [ ] 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-02-05 10:29:19 +08:00
Mike Hall
5466ab6cf8 CursorWrap improvements (#44936)
## Summary of the Pull Request
- Updated engine for better multi-monitor support.
- Closing the laptop lid will now update the monitor topology
- New settings/dropdown to support wrapping on horizontal, vertical, or
both

<img width="1103" height="643" alt="image"
src="https://github.com/user-attachments/assets/ff4f0835-a8ca-4603-9441-123b71747d5c"
/>

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

- [x] Closes: #44820
- [x] Closes: #44864
- [x] Closes: #44952

- [ ] **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
Feedback for CursorWrap shows that users want the ability to constrain
wrapping for horizontal only, vertical only, or both (default behavior).
This PR adds a new dropdown to CursorWrap settings to enable a user to
select the appropriate wrapping model.

## Validation Steps Performed
Local build and running on Surface Laptop 7 Pro - will also validate on
a multi-monitor setup.

---------

Co-authored-by: vanzue <vanzue@outlook.com>
2026-01-27 13:28:31 +08:00
Heiko
bf19bdc1ee [Enterprise; Policy] Add policy for CursorWrap to ADMX (#45028)
<!-- 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

Added missing policy definition.

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

- [x] Closes: #44897
<!-- - [ ] 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
- [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)
- [x] **Documentation updated:** See PR for issue #44484 

<!-- 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-01-27 11:42:06 +08:00
Jiří Polášek
2441621b80 CmdPal: Remove deadlock bait from AppListItem (#45076)
## Summary of the Pull Request

This PR removes a Task.Wait() call from lazy-loading AppListItem details
that could be invoked on the UI thread and lead to a deadlock.

It now follows the same pattern previously used for loading icons in the
same class, which has proven to work well.

Prevents #44938 from stepping on this landmine.

Cherry-picked from #44973.

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

- [x] Closes: #45074 
<!-- - [ ] 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-01-27 10:36:42 +08:00
Shawn Yuan
1ca9d10ff5 [Settings] [Advanced Paste] Upgrade advanced paste settings safely to fix settings ui crash (#44862)
<!-- 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 pull request makes a minor fix in the `AdvancedPasteViewModel`
constructor to ensure the correct settings repository is used for null
checking. The change improves code correctness by verifying
`advancedPasteSettingsRepository` instead of the generic
`settingsRepository`.

- Fixed null check to use `advancedPasteSettingsRepository` instead of
`settingsRepository` in the `AdvancedPasteViewModel` constructor for
more accurate validation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #44835
<!-- - [ ] 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
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [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-01-26 16:06:19 +08:00
Gordon Lam
b438f15f6e [Peek] Fix Space key triggering during file rename (#44845) (#44995)
Don't show error window when CurrentItem is null - just return silently.
This restores the original behavior where CaretVisible() detection in
GetSelectedItems() would suppress Peek by returning null, and no window
would be shown.

PR #44703 added an error window for virtual folders (Home/Recent), but
this also triggered when user was typing (rename, search, address bar),
stealing focus and cancelling the operation.

Fixes #44845

<!-- 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
2026-01-26 16:06:12 +08:00
Shawn Yuan
48de981f50 Add telemetry for tray icon (#44985)
<!-- 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 pull request adds telemetry tracking for user interactions with the
application's tray icon. Specifically, it introduces new methods for
logging `left-click`, `right-click`, and `double-click` events, and
integrates these telemetry calls into the tray icon event handling
logic.

<!-- 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
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [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-01-26 16:06:06 +08:00
Shawn Yuan
9ab6559fac [Settings] Fix right click menu display issue (#44982)
<!-- 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 pull request updates the tray icon context menu logic to better
reflect the state of the "Quick Access" feature. The menu now
dynamically updates its items and labels based on whether Quick Access
is enabled or disabled, improving clarity for users.

**Menu behavior improvements:**

* The tray icon menu now reloads itself when the Quick Access setting
changes, ensuring the menu always matches the current state.
* The "Settings" menu item label changes to "Settings\tLeft-click" when
Quick Access is disabled, providing clearer instructions to users.
[[1]](diffhunk://#diff-e5efbda4c356e159a6ca82a425db84438ab4014d1d90377b98a2eb6d9632d32dR176-R179)
[[2]](diffhunk://#diff-7139ecb2cf76e472c574a155268c19e919e2cce05d9d345c50c1f1bffc939e1aR198-R248)
* The Quick Access menu item is removed from the context menu when the
feature is disabled, preventing confusion.

**Internal state tracking:**

* Added a new variable `last_quick_access_state` to track the previous
Quick Access state and trigger menu reloads only when necessary.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

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

<!-- 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
- When Quick Access is disabled

<img width="1537" height="312" alt="image"
src="https://github.com/user-attachments/assets/5d51f24e-ccb4-4973-afaa-8b64cc35db87"
/>

- When Quick Access is enabled
<img width="1601" height="201" alt="image"
src="https://github.com/user-attachments/assets/56366d10-bcec-4892-b2d2-f8213ad726aa"
/>

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-01-26 16:05:58 +08:00
moooyo
8cc32d3098 fix: Improve Unicode normalization and add regex metachar tests (#44944)
Enhanced SanitizeAndNormalize to handle Unicode normalization more
robustly, ensuring correct buffer sizing and error handling. Added unit
tests for regex metacharacters `$` and `^` to verify correct replacement
behavior at string boundaries. Improves Unicode support and test
coverage for regex edge cases.

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

- [x] Closes: #44942 #44892
<!-- - [ ] 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>
2026-01-26 16:05:54 +08:00
Jiří Polášek
3b7eedfb67 CmdPal: Improve loading of application icons - part 1 (#44938)
## Summary of the Pull Request

This PR improves loading of application icons:

- Fixes loading of icons from internet shortcuts

## Pictures? Pictures!

<img width="683" height="399" alt="image"
src="https://github.com/user-attachments/assets/5e566648-7b1a-4254-8afd-557a321b19d6"
/>


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

- [x] Closes: #44819
<!-- - [ ] 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-01-26 16:05:49 +08:00
Kai Tao
d7e1b18ba4 Runner TrayIcon: Monochrome icon should adapt to windows theme instead of the app theme (#44931)
<!-- 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

- [X] Closes: #44891
<!-- - [ ] 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
============ System light + App Light
<img width="903" height="239" alt="image"
src="https://github.com/user-attachments/assets/581606fb-99b5-4df9-a520-545a0c04676c"
/>
============ System Light + App Dark
<img width="991" height="239" alt="image"
src="https://github.com/user-attachments/assets/822009e9-57cf-452b-b3aa-f1cbc25883f8"
/>
============ System Dark + App Light
<img width="932" height="236" alt="image"
src="https://github.com/user-attachments/assets/98a56d48-31f0-4f75-95a4-8c7dc83c3866"
/>
============ System Dark + App Dark
<img width="903" height="236" alt="image"
src="https://github.com/user-attachments/assets/2500a0d5-6b27-403e-89b4-69b7d3b91e79"
/>
============
2026-01-26 16:05:43 +08:00
Kai Tao
0206fdbec1 Cmdpal: use latest msix to install (#44886)
<!-- 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 should install latest cmdpal msix
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #44860
<!-- - [ ] 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-01-26 16:05:36 +08:00
Leilei Zhang
bdaf644f02 test sign 2026-01-19 14:29:46 +08:00
moooyo
329c8c2616 refactor(imageresizer): disable AI feature and cache functionality (#44759)
<!-- 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: Yu Leng <yuleng@microsoft.com>
2026-01-19 10:51:10 +08:00
Niels Laute
b081e413b1 Upgrade MarkdownTextBlock (#44793)
<!-- 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:

- upgrades `CommunityToolkit.WinUI.Labs.MarkdownTextBlock` to version
`0.1.260116-build.2514`. This update includes a bunch of improvements
for markdown rendering in its default config, and fixes a couple of bug
with regards to rendering large images getting clipped when resizing the
window.
- replaces an incorrect image in the `Command Palette Sample Page`
extension for the markdown + images sample.

Before vs after:

<img width="910" height="234" alt="image"
src="https://github.com/user-attachments/assets/b3dad76c-a89e-4b47-90f8-d3c64f00615f"
/>


<img width="1245" height="827" alt="image"
src="https://github.com/user-attachments/assets/00037fb5-453e-4d85-83c9-92c265b9f968"
/>



<!-- 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-01-19 09:56:30 +08:00
leileizhang
290fa01adf [ImageResizer] Temporarily disable AI Super Resolution feature (#44768)
<!-- 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
Temporarily disables the AI Super Resolution feature in Image Resizer
while keeping all code intact for re-enabling in a future release.

<!-- 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-01-17 11:28:47 +08:00
67 changed files with 1466 additions and 713 deletions

View File

@@ -416,7 +416,6 @@ DISPLAYFLAGS
DISPLAYFREQUENCY
displayname
DISPLAYORIENTATION
DISPLAYPORT
divyan
DLGFRAME
dlgmodalframe
@@ -863,7 +862,6 @@ jjw
jobject
JOBOBJECT
jpe
JPN
jpnime
jrsoftware
Jsons
@@ -996,7 +994,6 @@ LTM
LTRREADING
luid
lusrmgr
LVDS
LWA
LWIN
LZero
@@ -1061,8 +1058,6 @@ MINIMIZESTART
MINMAXINFO
minwindef
Mip
Miracast
miracast
mkdn
mlcfg
mmc
@@ -1391,6 +1386,7 @@ popups
POPUPWINDOW
portfile
POSITIONITEM
Postbot
POWERBROADCAST
powerdisplay
POWERDISPLAYMODULEINTERFACE
@@ -1821,7 +1817,6 @@ svchost
SVGIn
SVGIO
svgz
SVIDEO
SVSI
SWFO
swp

View File

@@ -1004,6 +1004,10 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/ShortcutGuide/ShortcutGuide.UnitTests/ShortcutGuide.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.vcxproj" Id="e487304a-b1fb-4e6b-8e70-014051af5b99" />
</Folder>
<Folder Name="/modules/Workspaces/">

View File

@@ -195,10 +195,18 @@ Special sections start with an identifier enclosed between `<` and `>`. This dec
A string array of all the keys that need to be pressed. If a number is supplied, it should be read as a [KeyCode](https://learn.microsoft.com/windows/win32/inputdev/virtual-key-codes) and displayed accordingly (based on the Keyboard Layout of the user).
**Literal digit keys**:
Because a bare number is interpreted as a virtual-key code, a literal digit key must be authored using the `<N>` notation (the digit enclosed between `<` and `>`), where `N` is `0``9`. For example, `<9>` represents the literal `9` key (as in the "switch to the last tab" shortcut), not the virtual-key code `9` (which is `Tab`). The interpreter strips the brackets and displays just the digit.
This applies only to a single literal digit. A range such as `1 - 8` is a free-form label, not a key, and is supplied verbatim (the brackets would only be trimmed from the ends, so `<1> - <8>` would not render as intended).
**Special keys**:
Special keys are enclosed between `<` and `>` and correspond to a key that should be displayed in a certain way. If the interpreter of the manifest file can't understand the content, the brackets should be left out.
By convention these tokens are written as double-quoted strings in the YAML (for example `"<Enter>"` and `"<9>"`), matching the quoting used for punctuation key values. YAML treats the quoted and unquoted forms identically, so quoting is for consistency rather than a strict requirement for bracketed tokens.
|Name|Description|
|----|-----------|
|`<Office>`| Corresponds to the Office key on some Windows keyboards |

View File

@@ -210,7 +210,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- SectionName: Formatting
Properties:
- Name: Bold

View File

@@ -1542,7 +1542,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 3
- "<3>"
- Name: Move earlier or later by number of frames specified for stroke Duration
Shortcut:
- Win: false

View File

@@ -642,7 +642,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 3
- "<3>"
- Name: Show document template
Shortcut:
- Win: false
@@ -810,7 +810,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 5
- "<5>"
- Name: Release guides
Shortcut:
- Win: false
@@ -818,7 +818,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 5
- "<5>"
- Name: Show/ hide smart guides
Shortcut:
- Win: false
@@ -925,7 +925,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 6
- "<6>"
- Name: Select the object above the current selection
Shortcut:
- Win: false
@@ -965,7 +965,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 2
- "<2>"
- Name: Unlock a selection
Shortcut:
- Win: false
@@ -973,7 +973,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Hide a selection
Shortcut:
- Win: false
@@ -981,7 +981,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 3
- "<3>"
- Name: Show all selections
Shortcut:
- Win: false
@@ -989,7 +989,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 3
- "<3>"
- Name: Move selection in user-defined increments
Shortcut:
- Win: false
@@ -1013,7 +1013,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 2
- "<2>"
- Name: Bring a selection forward
Shortcut:
- Win: false
@@ -1071,7 +1071,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 8
- "<8>"
- Name: Release a compound path
Shortcut:
- Win: false
@@ -1079,7 +1079,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 8
- "<8>"
- Name: Edit a pattern
Shortcut:
- Win: false
@@ -1261,7 +1261,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 4
- "<4>"
- Name: Move an object
Shortcut:
- Win: false
@@ -1285,7 +1285,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 7
- "<7>"
- Name: Release a clipping mask
Shortcut:
- Win: false
@@ -1293,7 +1293,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 7
- "<7>"
- Name: Toggle between fill and stroke
Shortcut:
- Win: false
@@ -1641,7 +1641,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 8
- "<8>"
- Name: Insert copyright symbol
Shortcut:
- Win: false
@@ -1665,7 +1665,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 7
- "<7>"
- Name: Insert section symbol
Shortcut:
- Win: false
@@ -1673,7 +1673,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 6
- "<6>"
- Name: Insert trademark symbol
Shortcut:
- Win: false
@@ -1681,7 +1681,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Insert registered trademark symbol
Shortcut:
- Win: false

View File

@@ -1036,7 +1036,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 5
- "<5>"
- Name: Redraw screen
Shortcut:
- Win: false
@@ -1060,7 +1060,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Switch to next/previous document window
Shortcut:
- Win: false
@@ -1155,7 +1155,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 6
- "<6>"
- Name: Toggle Character/Paragraph text attributes mode
Shortcut:
- Win: false
@@ -1163,7 +1163,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 7
- "<7>"
- Name: Display the pop-up menu that has focus
Shortcut:
- Win: false
@@ -1301,7 +1301,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 1
- "<1>"
- Name: Show Magenta plate
Shortcut:
- Win: false
@@ -1309,7 +1309,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 2
- "<2>"
- Name: Show Yellow plate
Shortcut:
- Win: false
@@ -1317,7 +1317,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 3
- "<3>"
- Name: Show Black plate
Shortcut:
- Win: false
@@ -1325,7 +1325,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 4
- "<4>"
- Name: Show 1st Spot plate
Shortcut:
- Win: false
@@ -1333,7 +1333,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 5
- "<5>"
- Name: Show 2nd Spot plate
Shortcut:
- Win: false
@@ -1341,7 +1341,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 6
- "<6>"
- Name: Show 3rd Spot plate
Shortcut:
- Win: false
@@ -1349,7 +1349,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 7
- "<7>"
- SectionName: Transform panel
Properties:
- Name: Apply value and copy object

View File

@@ -803,7 +803,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Switch to Hand tool (when not in text-edit mode)
Shortcut:
- Win: false
@@ -1309,7 +1309,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 1
- "<1>"
- Name: Tone Curve panel
Shortcut:
- Win: false
@@ -1317,7 +1317,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Detail panel
Shortcut:
- Win: false
@@ -1325,7 +1325,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 3
- "<3>"
- Name: HSL/Grayscale panel
Shortcut:
- Win: false
@@ -1333,7 +1333,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 4
- "<4>"
- Name: Split Toning panel
Shortcut:
- Win: false
@@ -1341,7 +1341,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 5
- "<5>"
- Name: Lens Corrections panel
Shortcut:
- Win: false
@@ -1349,7 +1349,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 6
- "<6>"
- Name: Camera Calibration panel
Shortcut:
- Win: false
@@ -1357,7 +1357,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 7
- "<7>"
- Name: Presets panel
Shortcut:
- Win: false
@@ -1365,7 +1365,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 9
- "<9>"
- Name: Open Snapshots panel
Shortcut:
- Win: false
@@ -1373,7 +1373,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 9
- "<9>"
- Name: Parametric Curve Targeted Adjustment tool
Shortcut:
- Win: false
@@ -1665,7 +1665,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 6
- "<6>"
- Name: (Filmstrip mode) Add yellow label
Shortcut:
- Win: false
@@ -1673,7 +1673,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 7
- "<7>"
- Name: (Filmstrip mode) Add green label
Shortcut:
- Win: false
@@ -1681,7 +1681,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 8
- "<8>"
- Name: (Filmstrip mode) Add blue label
Shortcut:
- Win: false
@@ -1689,7 +1689,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 9
- "<9>"
- Name: (Filmstrip mode) Add purple label
Shortcut:
- Win: false
@@ -1697,7 +1697,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 0
- "<0>"
- Name: Camera Raw preferences
Shortcut:
- Win: false
@@ -1936,7 +1936,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- Name: Cycle through blending modes
Shortcut:
- Win: false
@@ -2433,7 +2433,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Delete adjustment layer
Shortcut:
- Win: false

View File

@@ -407,7 +407,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Edge select mode
Shortcut:
- Win: false
@@ -415,7 +415,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 2
- "<2>"
- Name: Face select mode
Shortcut:
- Win: false
@@ -423,7 +423,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 3
- "<3>"
- Name: Extrude region
Shortcut:
- Win: false

View File

@@ -806,7 +806,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Set opacity to 50
Shortcut:
- Win: false
@@ -814,7 +814,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 5
- "<5>"
- Name: Set opacity to 100
Shortcut:
- Win: false
@@ -822,7 +822,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- SectionName: Arrange
Properties:
- Name: Bring forward

View File

@@ -489,7 +489,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- SectionName: Edit
Properties:
- Name: Undo

View File

@@ -63,7 +63,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Jump to rightmost tab
Shortcut:
- Win: false
@@ -71,7 +71,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 9
- "<9>"
- Name: Open home page in current tab
Shortcut:
- Win: false
@@ -424,7 +424,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- Name: Scroll down a screen
Shortcut:
- Win: false

View File

@@ -21,7 +21,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 1
- "<1>"
- Name: Show Intention Actions
Recommended: true
Shortcut:
@@ -778,7 +778,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 1
- "<1>"
- Name: Show Bookmarks window
Shortcut:
- Win: false
@@ -786,7 +786,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Show Find window
Shortcut:
- Win: false
@@ -794,7 +794,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 3
- "<3>"
- Name: Show Run window
Shortcut:
- Win: false
@@ -802,7 +802,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 4
- "<4>"
- Name: Show Debug window
Shortcut:
- Win: false
@@ -810,7 +810,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 5
- "<5>"
- Name: Show Problems window
Shortcut:
- Win: false
@@ -818,7 +818,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 6
- "<6>"
- Name: Show Structure window
Shortcut:
- Win: false
@@ -826,7 +826,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 7
- "<7>"
- Name: Show Services window
Shortcut:
- Win: false
@@ -834,7 +834,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 8
- "<8>"
- Name: Show Version Control window
Shortcut:
- Win: false
@@ -842,7 +842,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 9
- "<9>"
- Name: Show Commit window
Shortcut:
- Win: false
@@ -850,7 +850,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 0
- "<0>"
- Name: Show Terminal window
Shortcut:
- Win: false

View File

@@ -45,7 +45,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Switch to the last tab
Shortcut:
- Win: false
@@ -53,7 +53,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 9
- "<9>"
- Name: Close the current tab
Shortcut:
- Win: false
@@ -479,7 +479,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- Name: Stop loading page; close dialog or pop-up
Shortcut:
- Win: false

View File

@@ -492,7 +492,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Focus into Second Editor Group
Shortcut:
- Win: false
@@ -500,7 +500,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 2
- "<2>"
- Name: Focus into Third Editor Group
Shortcut:
- Win: false
@@ -508,7 +508,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 3
- "<3>"
- Name: Move Editor Left
Shortcut:
- Win: false

View File

@@ -202,7 +202,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- SectionName: Editing
Properties:
- Name: Copy
@@ -485,7 +485,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Go to last tab
Shortcut:
- Win: false
@@ -493,7 +493,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 9
- "<9>"
- Name: Move tab left
Shortcut:
- Win: false

View File

@@ -0,0 +1,463 @@
PackageName: Postman.Postman
Name: Postman
WindowFilter: "Postman.exe"
BackgroundProcess: false
Shortcuts:
- SectionName: Tabs
Properties:
- Name: Close tab
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- W
- Name: Force close tab
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- W
- Name: Switch to next tab
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- Tab
- Name: Switch to previous tab
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- Tab
- Name: Switch to tab at position (18)
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- '1 - 8'
- Name: Switch to last tab
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "<9>"
- Name: Reopen last closed tab
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- T
- Name: New runner tab
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- R
- Name: Search tabs
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- A
- SectionName: Sidebar
Properties:
- Name: Search sidebar
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- F
- Name: Next item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Down>"
- Name: Previous item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Up>"
- Name: Expand item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Right>"
- Name: Expand all
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: true
Keys:
- "<Right>"
- Name: Collapse item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Left>"
- Name: Collapse all
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: true
Keys:
- "<Left>"
- Name: Select item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Enter>"
- Name: Rename item
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- E
- Name: Cut item
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- X
- Name: Copy item
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- C
- Name: Paste item
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- V
- Name: Duplicate item
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- D
- Name: Delete item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Delete>"
- SectionName: Request
Properties:
- Name: Request URL
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- L
- Name: Save request
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- S
- Name: Save request as
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- S
- Name: Send request
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "<Enter>"
- Name: Send and download request
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- "<Enter>"
- SectionName: Interface
Properties:
- Name: Zoom in
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- Plus
- Name: Zoom out
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- Minus
- Name: Reset zoom
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "<0>"
- Name: Toggle two-pane view
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- V
- Name: Toggle left sidebar
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "\\"
- Name: Toggle right sidebar
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- "\\"
- Name: Toggle workbench
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- M
- Name: Swap sidebars
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- S
- Name: Reset layout
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- R
- Name: Environment selector
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: true
Keys:
- E
- SectionName: Window and modals
Properties:
- Name: New…
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- N
- Name: New Postman window
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- N
- Name: New console window
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- C
- Name: Find
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- F
- Name: Import
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- O
- Name: Settings
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- ","
- Name: Open shortcut help
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "/"
- Name: Search
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- K
- Name: Search in current workspace
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- K
- Name: Open Postbot
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- P
- Name: Open Vault
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- V
- Name: Open browser tab
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- B
- Name: Cancel conversation
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- C
- Name: Accept all
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- Y
- Name: Reject all
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "<Escape>"
- SectionName: Console
Properties:
- Name: Clear console
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- K
- Name: Show/hide console
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "`"

View File

@@ -241,7 +241,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 1
- "<1>"
- Name: Browse DMs
Shortcut:
- Win: false
@@ -249,7 +249,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 2
- "<2>"
- Name: Open the Activity view
Shortcut:
- Win: false
@@ -265,7 +265,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 0
- "<0>"
- Name: Open the Threads view
Shortcut:
- Win: false
@@ -525,7 +525,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 9
- "<9>"
- Name: Inline code selected text
Shortcut:
- Win: false
@@ -549,7 +549,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 8
- "<8>"
- Name: Numbered list
Shortcut:
- Win: false
@@ -557,7 +557,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 7
- "<7>"
- Name: Apply markdown formatting
Shortcut:
- Win: false
@@ -583,7 +583,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 0
- "<0>"
- Name: Big heading
Shortcut:
- Win: false
@@ -591,7 +591,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 1
- "<1>"
- Name: Medium heading
Shortcut:
- Win: false
@@ -599,7 +599,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Small heading
Shortcut:
- Win: false
@@ -607,7 +607,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 3
- "<3>"
- Name: Checklist
Shortcut:
- Win: false
@@ -615,7 +615,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 0
- "<0>"
- Name: Bulleted list
Shortcut:
- Win: false
@@ -623,7 +623,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 8
- "<8>"
- Name: Numbered list
Shortcut:
- Win: false
@@ -631,7 +631,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 7
- "<7>"
- Name: Toggle heading and list styles
Shortcut:
- Win: false

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using ShortcutGuide.Models;
@@ -32,16 +33,27 @@ namespace ShortcutGuide.Helpers
list.Add(shortcutEntry);
}
// Persist on a best-effort basis. The in-memory pinned list is the source of truth
// for the rest of the session; failing to write should not crash the overlay
// (Pin/Unpin runs from a synchronous UI handler).
Save();
PinnedShortcutsChanged?.Invoke(null, appName);
}
public static void Save()
{
string serialized = JsonSerializer.Serialize(App.PinnedShortcuts);
string pinnedPath = SettingsUtils.Default.GetSettingsFilePath(ShortcutGuideSettings.ModuleName, "Pinned.json");
File.WriteAllText(pinnedPath, serialized);
try
{
string serialized = JsonSerializer.Serialize(App.PinnedShortcuts);
string pinnedPath = SettingsUtils.Default.GetSettingsFilePath(ShortcutGuideSettings.ModuleName, "Pinned.json");
File.WriteAllText(pinnedPath, serialized);
}
catch (Exception ex) when (ex is IOException
or UnauthorizedAccessException
or JsonException)
{
Logger.LogError("Failed to persist Shortcut Guide pinned shortcuts; keeping in-memory state.", ex);
}
}
}
}

View File

@@ -2,9 +2,12 @@
// 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.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
@@ -31,21 +34,39 @@ namespace ShortcutGuide
public App()
{
this.InitializeComponent();
// Register process-wide exception handlers so a stray exception (e.g. an IO failure
// during a fire-and-forget UI handler, or a background Task fault) gets logged
// instead of taking the overlay down with an unhandled access violation in coreclr.
// Without these the runtime tears the process down before our local catches can run.
this.UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
this.LoadData();
MainWindow = new MainWindow();
TaskBarWindow = new TaskbarWindow();
MainWindow.Activate();
MainWindow.Closed += (_, _) =>
try
{
PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSessionEvent(
MainWindow.SessionDurationMs,
MainWindow.CloseType));
TaskBarWindow.Close();
};
this.LoadData();
MainWindow = new MainWindow();
TaskBarWindow = new TaskbarWindow();
MainWindow.Activate();
MainWindow.Closed += (_, _) =>
{
PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSessionEvent(
MainWindow.SessionDurationMs,
MainWindow.CloseType));
TaskBarWindow.Close();
};
}
catch (Exception ex)
{
// Any failure in launch is fatal for this short-lived overlay; log and exit
// cleanly rather than letting WinUI surface a generic crash dialog.
Logger.LogError("Failed to launch Shortcut Guide.", ex);
Environment.Exit(1);
}
}
private void LoadData()
@@ -63,18 +84,53 @@ namespace ShortcutGuide
PinnedShortcuts = loaded;
}
}
catch (JsonException)
catch (Exception ex) when (ex is JsonException
or IOException
or UnauthorizedAccessException)
{
// Fall back to the empty default if the file is corrupt.
// Fall back to the empty default if the file is corrupt or unreadable.
Logger.LogWarning($"Failed to load pinned shortcuts from '{pinnedPath}'. Falling back to empty list. Reason: {ex.Message}");
}
}
ShortcutGuideSettings = SettingsRepository<ShortcutGuideSettings>.GetInstance(settingsUtils).SettingsConfig;
ShortcutGuideProperties = ShortcutGuideSettings.Properties;
try
{
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
settingsUtils.SaveSettings(JsonSerializer.Serialize(App.ShortcutGuideSettings, new JsonSerializerOptions { WriteIndented = true }), "Shortcut Guide");
settingsUtils.SaveSettings(JsonSerializer.Serialize(App.ShortcutGuideSettings, new JsonSerializerOptions { WriteIndented = true }), "Shortcut Guide");
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// Persisting the round-tripped settings is best-effort; the in-memory copy is still valid.
Logger.LogWarning($"Failed to persist Shortcut Guide settings on launch. Reason: {ex.Message}");
}
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// Exceptions raised on the UI thread land here. Mark handled so the runtime
// does not terminate the process; the overlay can usually continue.
Logger.LogError("Unhandled UI exception in Shortcut Guide.", e.Exception);
e.Handled = true;
}
private static void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
{
// Background-thread exceptions reach here as a last resort; we cannot prevent
// termination when IsTerminating is true, but at least we leave a log trail.
if (e.ExceptionObject is Exception ex)
{
Logger.LogError($"Unhandled background exception in Shortcut Guide (IsTerminating={e.IsTerminating}).", ex);
}
}
private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
Logger.LogError("Unobserved Task exception in Shortcut Guide.", e.Exception);
e.SetObserved();
}
}
}

View File

@@ -237,37 +237,54 @@ namespace ShortcutGuide
private void SetWindowPosition()
{
if (!this._hasMovedToRightMonitor)
try
{
NativeMethods.GetCursorPos(out NativeMethods.POINT lpPoint);
AppWindow.Move(new NativeMethods.POINT { Y = lpPoint.Y - ((int)Height / 2), X = lpPoint.X - ((int)Width / 2) });
this._hasMovedToRightMonitor = true;
if (!this._hasMovedToRightMonitor)
{
NativeMethods.GetCursorPos(out NativeMethods.POINT lpPoint);
AppWindow.Move(new NativeMethods.POINT { Y = lpPoint.Y - ((int)Height / 2), X = lpPoint.X - ((int)Width / 2) });
this._hasMovedToRightMonitor = true;
}
var hwnd = WindowNative.GetWindowHandle(this);
float dpi = DpiHelper.GetDPIScaleForWindow(hwnd);
Rect monitorRect = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd);
var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value;
// App.TaskBarWindow / its AppWindow can briefly be null during the reentrant
// Hide → Activate → BringToFront chain triggered from SelectionChanged. When the
// taskbar window is not currently observable, skip the overlap adjustment instead
// of crashing the overlay (issue #48448).
var taskbarWindow = App.TaskBarWindow?.AppWindow;
bool taskbarOnLeft = false;
bool taskbarOnRight = false;
if (taskbarWindow is not null)
{
taskbarOnLeft = taskbarWindow.IsVisible && taskbarWindow.Position.X < AppWindow.Position.X + Width && windowPosition == ShortcutGuideWindowPosition.Left;
taskbarOnRight = taskbarWindow.IsVisible && taskbarWindow.Position.X + taskbarWindow.Size.Width > AppWindow.Position.X && windowPosition == ShortcutGuideWindowPosition.Right;
}
double newHeight = monitorRect.Height / dpi;
if (taskbarWindow is not null && (taskbarOnLeft || taskbarOnRight))
{
newHeight -= taskbarWindow.Size.Height;
}
MaxHeight = newHeight;
MinHeight = newHeight;
Height = newHeight;
int xPosition = windowPosition == ShortcutGuideWindowPosition.Right
? (int)(monitorRect.X + monitorRect.Width) - (int)(Width * dpi)
: (int)monitorRect.X;
this.MoveAndResize(xPosition, (int)monitorRect.Y, Width, Height);
}
var hwnd = WindowNative.GetWindowHandle(this);
float dpi = DpiHelper.GetDPIScaleForWindow(hwnd);
Rect monitorRect = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd);
var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value;
var taskbarWindow = App.TaskBarWindow.AppWindow;
bool taskbarOnLeft = taskbarWindow.IsVisible && taskbarWindow.Position.X < AppWindow.Position.X + Width && windowPosition == ShortcutGuideWindowPosition.Left;
bool taskbarOnRight = taskbarWindow.IsVisible && taskbarWindow.Position.X + taskbarWindow.Size.Width > AppWindow.Position.X && windowPosition == ShortcutGuideWindowPosition.Right;
double newHeight = monitorRect.Height / dpi;
if (taskbarOnLeft || taskbarOnRight)
catch (Exception ex)
{
newHeight -= taskbarWindow.Size.Height;
Logger.LogError("Failed to set Shortcut Guide window position; keeping previous layout.", ex);
}
MaxHeight = newHeight;
MinHeight = newHeight;
Height = newHeight;
int xPosition = windowPosition == ShortcutGuideWindowPosition.Right
? (int)(monitorRect.X + monitorRect.Width) - (int)(Width * dpi)
: (int)monitorRect.X;
this.MoveAndResize(xPosition, (int)monitorRect.Y, Width, Height);
}
/// <summary>
@@ -282,25 +299,35 @@ namespace ShortcutGuide
return;
}
this._selectedAppName = selectedItem.Name;
App.CurrentAppName = this._selectedAppName;
this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName);
App.TaskBarWindow.Hide();
if (this._shortcutFile is ShortcutFile file)
try
{
// Show the taskbar button window only when the selected app exposes the <TASKBAR1-9> section.
if (file.Shortcuts is not null && file.Shortcuts.Any(c => c.SectionName?.StartsWith("<TASKBAR1-9>", StringComparison.Ordinal) == true))
{
this._taskBarWindowActivated = true;
App.TaskBarWindow.Activate();
}
this._selectedAppName = selectedItem.Name;
App.CurrentAppName = this._selectedAppName;
this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName);
// Reposition before navigating so the taskbar window does not clip into the main window.
this.SetWindowPosition();
this.ContentFrame.Navigate(
typeof(ShortcutsPage),
new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName });
App.TaskBarWindow?.Hide();
if (this._shortcutFile is ShortcutFile file)
{
// Show the taskbar button window only when the selected app exposes the <TASKBAR1-9> section.
if (file.Shortcuts is not null && file.Shortcuts.Any(c => c.SectionName?.StartsWith("<TASKBAR1-9>", StringComparison.Ordinal) == true))
{
this._taskBarWindowActivated = true;
App.TaskBarWindow?.Activate();
}
// Reposition before navigating so the taskbar window does not clip into the main window.
this.SetWindowPosition();
this.ContentFrame.Navigate(
typeof(ShortcutsPage),
new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName });
}
}
catch (Exception ex)
{
// Guard against exceptions during section navigation so the overlay does not close on the user.
// InitializeNavItemsAsync's catch interprets any exception bubbling out of the initial
// SelectedItem assignment as a fatal init failure and closes the window (issue #48448).
Logger.LogError($"Failed to handle Shortcut Guide section selection '{selectedItem.Name}'.", ex);
}
}

View File

@@ -30,54 +30,73 @@ namespace ShortcutGuide.ShortcutGuideXAML
public void UpdateTasklistButtons()
{
// This move ensures the window spawns on the same monitor as the main window
AppWindow.MoveInZOrderAtBottom();
AppWindow.Move(App.MainWindow.AppWindow.Position);
TasklistButton[] buttons = [];
// Wrap the entire body: this method runs from the ctor and from `Activated`,
// both of which can fire while MainWindow is closing or AppWindow is in a
// transient null state. An exception here used to crash the overlay because
// there was no caller-side try/catch (issue #48441).
try
{
buttons = TasklistPositions.GetButtons();
// This move ensures the window spawns on the same monitor as the main window.
// App.MainWindow / its AppWindow can briefly be null during the reentrant
// Hide → Activate → BringToFront chain triggered from SelectionChanged.
var mainAppWindow = App.MainWindow?.AppWindow;
if (mainAppWindow is null)
{
return;
}
AppWindow.MoveInZOrderAtBottom();
AppWindow.Move(mainAppWindow.Position);
TasklistButton[] buttons = [];
try
{
buttons = TasklistPositions.GetButtons();
}
catch (Exception ex)
{
Logger.LogError("Failed to enumerate taskbar buttons via TasklistPositions.GetButtons.", ex);
}
if (buttons.Length == 0)
{
AppWindow.Hide();
return;
}
float dpi = this.DPI;
double windowsLogoColumnWidth = this.WindowsLogoColumnWidth.Width.Value;
double windowHeight = 58;
double windowMargin = 8 * dpi;
double windowWidth = windowsLogoColumnWidth;
double xPosition = buttons[0].X - (windowsLogoColumnWidth * dpi);
double yPosition = this.WorkArea.Bottom - (windowHeight * dpi);
this.KeyHolder.Children.Clear();
foreach (TasklistButton b in buttons)
{
TaskbarIndicator indicator = new()
{
Label = b.Keynum >= 10 ? "0" : b.Keynum.ToString(CultureInfo.InvariantCulture),
Height = b.Height / dpi,
Width = b.Width / dpi,
};
windowWidth += indicator.Width;
this.KeyHolder.Children.Add(indicator);
double indicatorPos = (b.X - xPosition) / dpi;
Canvas.SetLeft(indicator, indicatorPos - windowsLogoColumnWidth);
}
this.MoveAndResize(xPosition - windowMargin, yPosition, windowWidth + (2 * windowMargin), windowHeight);
AppWindow.MoveInZOrderAtTop();
}
catch (Exception ex)
{
Logger.LogError("Failed to enumerate taskbar buttons via TasklistPositions.GetButtons.", ex);
Logger.LogError("Failed to update Shortcut Guide taskbar indicator window.", ex);
}
if (buttons.Length == 0)
{
AppWindow.Hide();
return;
}
float dpi = this.DPI;
double windowsLogoColumnWidth = this.WindowsLogoColumnWidth.Width.Value;
double windowHeight = 58;
double windowMargin = 8 * dpi;
double windowWidth = windowsLogoColumnWidth;
double xPosition = buttons[0].X - (windowsLogoColumnWidth * dpi);
double yPosition = this.WorkArea.Bottom - (windowHeight * dpi);
this.KeyHolder.Children.Clear();
foreach (TasklistButton b in buttons)
{
TaskbarIndicator indicator = new()
{
Label = b.Keynum >= 10 ? "0" : b.Keynum.ToString(CultureInfo.InvariantCulture),
Height = b.Height / dpi,
Width = b.Width / dpi,
};
windowWidth += indicator.Width;
this.KeyHolder.Children.Add(indicator);
double indicatorPos = (b.X - xPosition) / dpi;
Canvas.SetLeft(indicator, indicatorPos - windowsLogoColumnWidth);
}
this.MoveAndResize(xPosition - windowMargin, yPosition, windowWidth + (2 * windowMargin), windowHeight);
AppWindow.MoveInZOrderAtTop();
}
}
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ShortcutGuide.Converters;
using ShortcutGuide.Models;
namespace ShortcutGuide.UnitTests.ConvertersTests;
[TestClass]
public sealed class ShortcutDescriptionToKeysConverterTests
{
private static List<object> Convert(ShortcutDescription description)
=> new ShortcutDescriptionToKeysConverter().GetKeysList(description);
[TestMethod]
[DataRow("<0>")]
[DataRow("<1>")]
[DataRow("<8>")]
[DataRow("<9>")]
public void GetKeysList_LiteralDigitKey_IsPassedThroughVerbatim(string key)
{
// A literal digit key (e.g. Ctrl+9 "switch to last tab") is authored with the
// <N> convention so it is not parsed as a virtual-key code (VK 9 is Tab, VK 1 is
// the left mouse button, VK 0 is undefined). The converter forwards the token
// unchanged; KeyVisual strips the angle brackets when rendering.
var result = Convert(new ShortcutDescription(ctrl: true, shift: false, alt: false, win: false, keys: [key]));
CollectionAssert.AreEqual(new object[] { "Ctrl", key }, result);
}
[TestMethod]
public void GetKeysList_Modifiers_AreEmittedBeforeKeysInWinCtrlAltShiftOrder()
{
// Win -> 92, Ctrl -> "Ctrl", Alt -> "Alt", Shift -> 16, then the keys.
var result = Convert(new ShortcutDescription(ctrl: true, shift: true, alt: true, win: true, keys: ["A"]));
CollectionAssert.AreEqual(new object[] { 92, "Ctrl", "Alt", 16, "A" }, result);
}
[TestMethod]
public void GetKeysList_NonNumericKey_IsPassedThroughVerbatim()
{
// Non-numeric key strings (e.g. the "1 - 8" tab-range) render as-is.
var result = Convert(new ShortcutDescription(ctrl: true, shift: false, alt: false, win: false, keys: ["1 - 8"]));
CollectionAssert.AreEqual(new object[] { "Ctrl", "1 - 8" }, result);
}
[TestMethod]
public void GetKeysList_ArrowNameKey_MapsToVirtualKeyCode()
{
// Named arrow keys map to their VK codes (Up -> 38), independent of the digit handling.
var result = Convert(new ShortcutDescription(ctrl: false, shift: false, alt: false, win: false, keys: ["Up"]));
CollectionAssert.AreEqual(new object[] { 38 }, result);
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<SelfContained>true</SelfContained>
<RuntimeIdentifier Condition="'$(Platform)' == 'x64'">win-x64</RuntimeIdentifier>
<RuntimeIdentifier Condition="'$(Platform)' == 'ARM64'">win-arm64</RuntimeIdentifier>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\ShortcutGuide.UnitTests\</OutputPath>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ShortcutGuide.Ui\ShortcutGuide.Ui.csproj" />
</ItemGroup>
</Project>

View File

@@ -1089,13 +1089,10 @@ VideoRecordingSession::VideoRecordingSession(
// Store frame interval for timeout-based frame production when webcam is active.
m_frameIntervalTicks = ( frameRate > 0 ) ? ( 10'000'000LL / frameRate ) : 333'333LL;
if (captureAudio || captureSystemAudio)
{
// Always set up audio profile for loopback capture (stereo AAC)
auto audio = m_encodingProfile.Audio();
audio = winrt::AudioEncodingProperties::CreateAac(48000, 2, 192000);
m_encodingProfile.Audio(audio);
}
// NOTE: Audio encoding profile (m_encodingProfile.Audio) is set in
// StartAsync() after the audio graph is fully initialized, not here.
// Calling GetEncodingProperties() before InitializeAsync completes
// would crash because m_audioOutputNode is still null.
// Describe our input: uncompressed BGRA8 buffers
auto properties = winrt::VideoEncodingProperties::CreateUncompressed(
@@ -1176,7 +1173,16 @@ winrt::IAsyncAction VideoRecordingSession::StartAsync()
RecDiag( L"StartAsync: co_await InitializeAsync...\n" );
co_await m_audioGenerator->InitializeAsync();
RecDiag( L"StartAsync: audio initialized\n" );
m_streamSource = winrt::MediaStreamSource(m_videoDescriptor, winrt::AudioStreamDescriptor(m_audioGenerator->GetEncodingProperties()));
// Set up the audio encoding profile now that the audio graph is
// fully initialized. GetEncodingProperties() requires
// m_audioOutputNode to be valid, which is only guaranteed after
// InitializeAsync completes.
auto audioProps = m_audioGenerator->GetEncodingProperties();
m_encodingProfile.Audio(winrt::AudioEncodingProperties::CreateAac(
audioProps.SampleRate(), audioProps.ChannelCount(), 192000));
m_streamSource = winrt::MediaStreamSource(m_videoDescriptor, winrt::AudioStreamDescriptor(audioProps));
}
else {

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -20,6 +19,9 @@ public record AppStateModel
init => _recentCommands = value;
}
// HERE BE DRAGONS: Using an ImmutableList<T> for a setting may explode in
// AOT builds. Make sure to test IN AOT setting this setting to null, [],
// and and array with values.
private ImmutableList<string>? _runHistory = ImmutableList<string>.Empty;
public ImmutableList<string> RunHistory

View File

@@ -427,12 +427,39 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
var title = _commandItemViewModel?.Title ?? string.Empty;
var subtitle = _commandItemViewModel?.Subtitle ?? string.Empty;
var icon = _commandItemViewModel?.Icon;
var dockSide = _settingsService.Settings.DockSettings.Side;
IReadOnlyList<MonitorInfo>? monitors = _monitorService?.GetMonitors();
var dockSettings = _settingsService.Settings.DockSettings;
var dockSide = dockSettings.Side;
IReadOnlyList<MonitorInfo>? monitors = GetDockEnabledMonitors(_monitorService, dockSettings);
ShowPinToDockDialogMessage message = new(_providerId, _commandId, title, subtitle, icon, dockSide, monitors);
WeakReferenceMessenger.Default.Send(message);
}
// Only list monitors where the dock is currently enabled, so users can't
// pin a command to a display that has no dock visible.
private static IReadOnlyList<MonitorInfo>? GetDockEnabledMonitors(IMonitorService? monitorService, DockSettings dockSettings)
{
var monitors = monitorService?.GetMonitors();
if (monitors is null)
{
return null;
}
var configs = dockSettings.MonitorConfigs;
// When there are no per-monitor configs (legacy / first-run), the dock
// is only shown on the primary monitor.
if (configs.Count == 0)
{
return monitors.Where(m => m.IsPrimary).ToList();
}
return monitors
.Where(m => configs.Any(c =>
string.Equals(c.MonitorDeviceId, m.StableId, System.StringComparison.OrdinalIgnoreCase) &&
c.Enabled))
.ToList();
}
private void UnpinFromDock()
{
PinToDockMessage message = new(_providerId, _commandId, false);

View File

@@ -24,9 +24,21 @@ internal sealed class RunHistoryService : IRunHistoryService
if (_appStateService.State.RunHistory.IsEmpty)
{
var history = Microsoft.Terminal.UI.RunHistory.CreateRunHistory();
// Copy the WinRT-projected IVector<string> into a plain List<string>
// before building the ImmutableList. ImmutableList.CreateRange tries to
// cast the source to IReadOnlyCollection<string>, which requires a WinRT
// helper type that isn't available in AOT builds and throws
// NotSupportedException.
var historyList = new List<string>(history.Count);
for (var i = 0; i < history.Count; i++)
{
historyList.Add(history[i]);
}
_appStateService.UpdateState(state => state with
{
RunHistory = history.ToImmutableList(),
RunHistory = historyList.ToImmutableList(),
});
}

View File

@@ -134,6 +134,25 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
MoreCommands = _networkPage.Commands,
};
if (isBandPage)
{
_networkUpItem = new ListItem(_networkPage)
{
Title = $"{_networkUpSpeed}",
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
Icon = Icons.NetworkUpIcon,
MoreCommands = _networkPage.Commands,
};
_networkDownItem = new ListItem(_networkPage)
{
Title = $"{_networkDownSpeed}",
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
Icon = Icons.NetworkDownIcon,
MoreCommands = _networkPage.Commands,
};
}
_networkPage.Updated += (s, e) =>
{
_networkItem.Title = _networkPage.GetItemTitle(isBandPage);
@@ -253,22 +272,6 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
}
else
{
_networkUpItem = new ListItem(_networkPage!)
{
Title = $"{_networkUpSpeed}",
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
Icon = Icons.NetworkUpIcon,
MoreCommands = _networkPage!.Commands,
};
_networkDownItem = new ListItem(_networkPage!)
{
Title = $"{_networkDownSpeed}",
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
Icon = Icons.NetworkDownIcon,
MoreCommands = _networkPage!.Commands,
};
return _batteryItem is not null
? new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem!, _batteryItem! }
: new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem! };

View File

@@ -94,7 +94,7 @@ internal static class Commands
})
{
Title = Resources.Microsoft_plugin_sys_hibernate,
Icon = Icons.SleepIcon, // Icon change needed
Icon = Icons.HibernateIcon,
},
});

View File

@@ -25,4 +25,6 @@ internal sealed class Icons
internal static IconInfo ShutdownIcon { get; } = new IconInfo("\uE7E8");
internal static IconInfo SleepIcon { get; } = new IconInfo("\uE708");
internal static IconInfo HibernateIcon { get; } = new IconInfo("\uE823");
}

View File

@@ -243,5 +243,7 @@ namespace ColorPicker.Helpers
lpPoint.Y += yOffset;
SetCursorPos(lpPoint.X, lpPoint.Y);
}
internal IntPtr GetMainWindowHandle() => _hwndSource?.Handle ?? IntPtr.Zero;
}
}

View File

@@ -0,0 +1,51 @@
// 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.Runtime.InteropServices;
using ManagedCommon;
namespace ColorPicker.Helpers;
internal static class WindowCaptureExclusionHelper
{
// Windows 10 version 2004 (build 19041) is the minimum supported version. PowerToys
// itself requires the same version, so this check is not strictly required, but is
// useful as a safeguard.
private static readonly bool IsSupported =
Environment.OSVersion.Version >= new Version(10, 0, 19041);
// Only logging once per session to avoid repeated identical warnings, as the zoom
// window may be used very often.
private static bool hasLoggedFailure;
internal static bool Exclude(IntPtr hwnd) =>
SetWindowAffinity(hwnd, NativeMethods.WDA_EXCLUDEFROMCAPTURE);
internal static bool Include(IntPtr hwnd) =>
SetWindowAffinity(hwnd, NativeMethods.WDA_NONE);
private static bool SetWindowAffinity(nint hwnd, uint affinity)
{
if (!IsSupported)
{
return false;
}
bool success = NativeMethods.SetWindowDisplayAffinity(hwnd, affinity);
if (!success)
{
int errorCode = Marshal.GetLastWin32Error();
if (!hasLoggedFailure)
{
Logger.LogWarning(
$"Failed to set window display affinity. Error code: {errorCode}");
hasLoggedFailure = true;
}
}
return success;
}
}

View File

@@ -79,12 +79,30 @@ namespace ColorPicker.Helpers
// we just started zooming, copy screen area
if (_previousZoomLevel == 0)
{
var x = (int)point.X - (BaseZoomImageSize / 2);
var y = (int)point.Y - (BaseZoomImageSize / 2);
// First, exclude the color picker window from the capture; otherwise its
// corner will be included in the zoomed-in image.
var mainWindowHandle = _appStateHandler.GetMainWindowHandle();
bool exclusionSuccess =
WindowCaptureExclusionHelper.Exclude(mainWindowHandle);
_graphics.CopyFromScreen(x, y, 0, 0, _bmp.Size, CopyPixelOperation.SourceCopy);
try
{
var x = (int)point.X - (BaseZoomImageSize / 2);
var y = (int)point.Y - (BaseZoomImageSize / 2);
_zoomViewModel.ZoomArea = BitmapToImageSource(_bmp);
_graphics.CopyFromScreen(x, y, 0, 0, _bmp.Size, CopyPixelOperation.SourceCopy);
_zoomViewModel.ZoomArea = BitmapToImageSource(_bmp);
}
finally
{
// Restore the color picker window to normal display affinity so that
// it can be captured again.
if (exclusionSuccess)
{
WindowCaptureExclusionHelper.Include(mainWindowHandle);
}
}
}
_zoomViewModel.ZoomFactor = Math.Pow(ZoomFactor, _currentZoomLevel - 1);

View File

@@ -231,5 +231,17 @@ namespace ColorPicker
var hwnd = new WindowInteropHelper(win).Handle;
_ = SetWindowLong(hwnd, GWL_EX_STYLE, GetWindowLong(hwnd, GWL_EX_STYLE) | WS_EX_TOOLWINDOW);
}
/// <summary>
/// Sets the display affinity of a window, which controls how the window is
/// displayed on a monitor. Used to exclude the picker window from ZoomWindow's
/// source bitmap.
/// </summary>
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint dwAffinity);
internal const uint WDA_NONE = 0x00000000;
internal const uint WDA_EXCLUDEFROMCAPTURE = 0x00000011;
}
}

View File

@@ -139,6 +139,14 @@ namespace KeyboardEventHandlers
if (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN)
{
ResetIfModifierKeyForLowerLevelKeyHandlers(ii, it->first, target);
// If a Ctrl/Alt/Shift key is remapped to a non-modifier key, reset the modifier state to prevent the injected key from being delivered as WM_SYSKEYDOWN instead of WM_KEYDOWN
if (Helpers::IsModifierKey(it->first) && !Helpers::IsModifierKey(target) && target != VK_CAPITAL && !(it->first == VK_LWIN || it->first == VK_RWIN || it->first == CommonSharedConstants::VK_WIN_BOTH))
{
std::vector<INPUT> suppressList;
Helpers::SetKeyEvent(suppressList, INPUT_KEYBOARD, static_cast<WORD>(it->first), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG);
ii.SendVirtualInput(suppressList);
}
}
if (remapToKey)

View File

@@ -226,6 +226,27 @@ namespace RemappingLogicTests
Assert::AreEqual(1, mockedInputHandler.GetSendVirtualInputCallCount());
}
// Test if SendVirtualInput is sent exactly once with the suppress flag when a Ctrl/Alt/Shift key is remapped to a non-modifier key
TEST_METHOD (HandleSingleKeyRemapEvent_ShouldSendVirtualInputWithSuppressFlagExactlyOnce_WhenCtrlAltShiftIsMappedToNonModifierKey)
{
mockedInputHandler.SetSendVirtualInputTestHandler([](LowlevelKeyboardEvent* data) {
if (data->lParam->dwExtraInfo == KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG)
return true;
else
return false;
});
testState.AddSingleKeyRemap(VK_LMENU, (DWORD)VK_BACK);
std::vector<INPUT> inputs{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_LMENU } },
};
mockedInputHandler.SendVirtualInput(inputs);
Assert::AreEqual(1, mockedInputHandler.GetSendVirtualInputCallCount());
}
// Test if correct keyboard states are set for a single key to two key shortcut remap
TEST_METHOD (RemappedKeyToTwoKeyShortcut_ShouldSetTargetKeyState_OnKeyEvent)
{

View File

@@ -22,6 +22,8 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.VSCodeHelper
public string AppData { get; set; } = string.Empty;
public string SharedStorageDbPath { get; set; } = string.Empty;
public ImageSource WorkspaceIcon() => WorkspaceIconBitMap;
public ImageSource RemoteIcon() => RemoteIconBitMap;

View File

@@ -16,6 +16,7 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.VSCodeHelper
public static class VSCodeInstances
{
private static readonly string _userAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
private static readonly string _userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
public static List<VSCodeInstance> Instances { get; set; } = new List<VSCodeInstance>();
@@ -129,6 +130,7 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.VSCodeHelper
var portableData = Path.Join(iconPath, "data");
instance.AppData = Directory.Exists(portableData) ? Path.Join(portableData, "user-data") : Path.Combine(_userAppDataPath, version);
instance.SharedStorageDbPath = GetSharedStorageDbPath(version, iconPath, Directory.Exists(portableData));
var vsCodeIconPath = Path.Join(iconPath, $"{version}.exe");
if (!File.Exists(vsCodeIconPath))
{
@@ -157,5 +159,30 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.VSCodeHelper
Instances.Add(instance);
}
}
private static string GetSharedStorageDbPath(string version, string iconPath, bool isPortable)
{
if (isPortable)
{
return Path.Join(iconPath, "data-shared", "sharedStorage", "state.vscdb");
}
var sharedStorageDirectory = version switch
{
"Code" => ".vscode-shared",
"Code - Insiders" => ".vscode-insiders-shared",
"Code - Exploration" => ".vscode-exploration-shared",
"VSCodium" => ".vscodium-shared",
"VSCodium - Insiders" => ".vscodium-insiders-shared",
_ => string.Empty,
};
if (string.IsNullOrEmpty(sharedStorageDirectory))
{
return string.Empty;
}
return Path.Combine(_userProfilePath, sharedStorageDirectory, "sharedStorage", "state.vscdb");
}
}
}

View File

@@ -97,6 +97,7 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper
// User/globalStorage/state.vscdb - history.recentlyOpenedPathsList - vscode v1.64 or later
var vscode_storage_db = Path.Combine(vscodeInstance.AppData, "User/globalStorage/state.vscdb");
var vscode_shared_storage_db = vscodeInstance.SharedStorageDbPath;
if (File.Exists(vscode_storage))
{
@@ -104,17 +105,37 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper
results.AddRange(storageResults);
}
if (File.Exists(vscode_storage_db))
var storageDbPaths = new[] { vscode_storage_db, vscode_shared_storage_db }
.Where(filePath => !string.IsNullOrEmpty(filePath))
.Distinct(StringComparer.OrdinalIgnoreCase);
foreach (var storageDbPath in storageDbPaths)
{
var storageDbResults = GetWorkspacesInVscdb(vscodeInstance, vscode_storage_db);
results.AddRange(storageDbResults);
if (File.Exists(storageDbPath))
{
var storageDbResults = GetWorkspacesInVscdb(vscodeInstance, storageDbPath);
results.AddRange(storageDbResults);
}
}
}
return results;
return results
.Where(workspace => workspace != null)
.GroupBy(GetWorkspaceKey, StringComparer.OrdinalIgnoreCase)
.Select(workspaceGroup => workspaceGroup.First())
.ToList();
}
}
private static string GetWorkspaceKey(VSCodeWorkspace workspace)
{
return string.Join(
"|",
workspace.VSCodeInstance?.ExecutablePath ?? string.Empty,
workspace.WorkspaceType,
workspace.Path ?? string.Empty);
}
private List<VSCodeWorkspace> GetWorkspacesInJson(VSCodeInstance vscodeInstance, string filePath)
{
var storageFileResults = new List<VSCodeWorkspace>();

View File

@@ -1,48 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Common.Drivers;
namespace PowerDisplay.UnitTests;
[TestClass]
public class DisplayClassifierTests
{
[DataTestMethod]
// Internal: INTERNAL high-bit flag
[DataRow(0x80000000u, true, DisplayName = "INTERNAL bit only")]
[DataRow(0x8000000Bu, true, DisplayName = "INTERNAL | DISPLAYPORT_EMBEDDED")]
// Internal: documented embedded subtypes
[DataRow(11u, true, DisplayName = "DISPLAYPORT_EMBEDDED")]
[DataRow(13u, true, DisplayName = "UDI_EMBEDDED")]
// External: LVDS is not classified internal per docs
[DataRow(6u, false, DisplayName = "LVDS (not classified internal per docs)")]
// External: documented external connectors
[DataRow(5u, false, DisplayName = "HDMI")]
[DataRow(10u, false, DisplayName = "DISPLAYPORT_EXTERNAL")]
[DataRow(12u, false, DisplayName = "UDI_EXTERNAL")]
// External: virtual / wireless
[DataRow(15u, false, DisplayName = "MIRACAST")]
[DataRow(17u, false, DisplayName = "INDIRECT_VIRTUAL")]
// External: OTHER (-1) cast to uint
[DataRow(0xFFFFFFFFu, false, DisplayName = "OTHER (-1 cast to uint)")]
// External: unrecognized values default to external
[DataRow(0xDEADBEEFu, false, DisplayName = "Unknown value defaults to external")]
// External: INTERNAL flag combined with an undocumented subtype is treated as external
// (locks in the docstring's "INTERNAL | unknown subtype = external" rule).
[DataRow(0x80000007u, false, DisplayName = "INTERNAL | unknown subtype 7 (treated as external)")]
public void IsInternal_ReturnsExpectedClassification(uint outputTechnology, bool expected)
{
Assert.AreEqual(expected, DisplayClassifier.IsInternal(outputTechnology));
}
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Models;
namespace PowerDisplay.UnitTests;
[TestClass]
public class MonitorIdComparerTests
{
private const string Upper = @"\\?\DISPLAY#BOE0900#4&ABC&0&UID111";
private const string Lower = @"\\?\display#boe0900#4&abc&0&uid111";
private const string DifferentUid = @"\\?\DISPLAY#BOE0900#4&ABC&0&UID222";
[TestMethod]
public void Equal_IdsDifferingOnlyByCase_AreEqual()
{
Assert.IsTrue(MonitorIdComparer.Equal(Upper, Lower));
}
[TestMethod]
public void Equal_DistinctMonitors_AreNotEqual()
{
Assert.IsFalse(MonitorIdComparer.Equal(Upper, DifferentUid));
}
[TestMethod]
public void Equal_BothNull_AreEqual()
{
Assert.IsTrue(MonitorIdComparer.Equal(null, null));
}
[TestMethod]
public void Equal_NullVersusValue_AreNotEqual()
{
Assert.IsFalse(MonitorIdComparer.Equal(null, Upper));
}
[TestMethod]
public void Instance_IsCaseInsensitive_ForDictionaryKeys()
{
var set = new System.Collections.Generic.HashSet<string>(MonitorIdComparer.Instance) { Upper };
Assert.IsTrue(set.Contains(Lower), "A monitor-Id-keyed set must match regardless of casing");
Assert.IsFalse(set.Contains(DifferentUid));
}
}

View File

@@ -39,77 +39,78 @@ public class MonitorIdentityTests
}
[TestMethod]
public void PnpHardwareKeyFromDevicePath_ReturnsHardwareSegments()
{
var input = @"\\?\DISPLAY#BOE0900#4&40f4dee&0&UID8388688#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}";
var expected = @"BOE0900#4&40f4dee&0&UID8388688";
Assert.AreEqual(expected, MonitorIdentity.PnpHardwareKeyFromDevicePath(input));
}
[TestMethod]
public void PnpHardwareKeyFromInstanceName_StripsSuffixAndNormalizesSeparator()
public void FromInstanceName_StripsSuffixNormalizesSeparatorAndAddsPrefix()
{
var input = @"DISPLAY\BOE0900\4&40f4dee&0&UID8388688_0";
var expected = @"BOE0900#4&40f4dee&0&UID8388688";
var expected = @"\\?\DISPLAY#BOE0900#4&40f4dee&0&UID8388688";
Assert.AreEqual(expected, MonitorIdentity.PnpHardwareKeyFromInstanceName(input));
Assert.AreEqual(expected, MonitorIdentity.FromInstanceName(input));
}
[TestMethod]
public void PnpHardwareKey_CrossFormat_ProducesSameKey()
public void FromInstanceName_MatchesFromDevicePath_ForSamePhysicalMonitor()
{
// The whole point of the PnP key: a WMI InstanceName and the matching DevicePath
// for the same physical monitor must produce identical keys, so WMI brightness
// instances can be joined to QueryDisplayConfig targets with a single lookup —
// even on dual-internal-panel devices (Yoga Book 9i, Zenbook Duo) where the
// EdidId alone collides.
// The core invariant of the WMI<->QueryDisplayConfig join: a WMI InstanceName and
// the matching DevicePath for the same physical monitor must reduce to the identical
// Monitor.Id, so WMI brightness instances can be paired with inventory entries with a
// single lookup — even on dual-internal-panel devices (Yoga Book 9i, Zenbook Duo)
// where the EdidId alone collides.
var instanceName = @"DISPLAY\BOE0900\4&40f4dee&0&UID8388688_0";
var devicePath = @"\\?\DISPLAY#BOE0900#4&40f4dee&0&UID8388688#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}";
var keyFromInstance = MonitorIdentity.PnpHardwareKeyFromInstanceName(instanceName);
var keyFromDevicePath = MonitorIdentity.PnpHardwareKeyFromDevicePath(devicePath);
var idFromInstance = MonitorIdentity.FromInstanceName(instanceName);
Assert.AreEqual(keyFromInstance, keyFromDevicePath);
Assert.IsFalse(string.IsNullOrEmpty(keyFromInstance), "expected non-empty key");
Assert.AreEqual(MonitorIdentity.FromDevicePath(devicePath), idFromInstance);
Assert.IsFalse(string.IsNullOrEmpty(idFromInstance), "expected non-empty id");
}
[TestMethod]
public void PnpHardwareKey_DualInternalPanel_DistinguishesByUid()
public void FromInstanceName_DiscreteGpuExternalReportedPanel_StillMatchesInventory()
{
// Issue #48587: on a dual-GPU laptop the built-in panel driven by the discrete GPU is
// reported by QueryDisplayConfig as DisplayPort-External, yet WmiMonitorBrightness
// still exposes it. The InstanceName and DevicePath captured in that state must reduce
// to the same Monitor.Id so WMI can claim the panel and brightness control keeps working.
var instanceName = @"DISPLAY\BOE0D79\5&1abcdef7&0&UID4352_0";
var devicePath = @"\\?\DISPLAY#BOE0D79#5&1abcdef7&0&UID4352#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}";
Assert.AreEqual(
MonitorIdentity.FromDevicePath(devicePath),
MonitorIdentity.FromInstanceName(instanceName));
}
[TestMethod]
public void FromInstanceName_DualInternalPanel_DistinguishesByUid()
{
// Yoga Book 9i style: two identical internal panels (same EdidId BOE0900) with
// different PnP UIDs. The PnP key must differ so the two WMI brightness instances
// each pair with the correct MonitorDisplayInfo.
// different PnP UIDs must reduce to different Monitor.Ids so the two WMI brightness
// instances each pair with the correct inventory entry.
var panelA = @"DISPLAY\BOE0900\4&abcdef&0&UID111_0";
var panelB = @"DISPLAY\BOE0900\4&abcdef&0&UID222_0";
Assert.AreNotEqual(
MonitorIdentity.PnpHardwareKeyFromInstanceName(panelA),
MonitorIdentity.PnpHardwareKeyFromInstanceName(panelB));
MonitorIdentity.FromInstanceName(panelA),
MonitorIdentity.FromInstanceName(panelB));
}
[TestMethod]
public void PnpHardwareKeyFromInstanceName_MultiDigitSuffix_StrippedCorrectly()
public void FromInstanceName_MultiDigitSuffix_StrippedCorrectly()
{
// WMI instance suffix can be _0, _1, _10, etc. — LastIndexOf('_') ensures we
// strip only the trailing suffix, not an underscore inside the UID itself.
// WMI instance suffix can be _0, _1, _10, etc. — LastIndexOf('_') ensures we strip
// only the trailing suffix, not an underscore inside the UID itself.
var input = @"DISPLAY\BOE0900\4&40f4dee&0&UID8388688_12";
var expected = @"BOE0900#4&40f4dee&0&UID8388688";
var expected = @"\\?\DISPLAY#BOE0900#4&40f4dee&0&UID8388688";
Assert.AreEqual(expected, MonitorIdentity.PnpHardwareKeyFromInstanceName(input));
Assert.AreEqual(expected, MonitorIdentity.FromInstanceName(input));
}
[TestMethod]
public void PnpHardwareKey_NullEmptyOrMalformed_ReturnsEmpty()
public void FromInstanceName_NullEmptyOrMalformed_ReturnsEmpty()
{
Assert.AreEqual(string.Empty, MonitorIdentity.PnpHardwareKeyFromDevicePath(null));
Assert.AreEqual(string.Empty, MonitorIdentity.PnpHardwareKeyFromDevicePath(string.Empty));
Assert.AreEqual(string.Empty, MonitorIdentity.PnpHardwareKeyFromDevicePath(@"\\?\DISPLAY"));
Assert.AreEqual(string.Empty, MonitorIdentity.PnpHardwareKeyFromInstanceName(null));
Assert.AreEqual(string.Empty, MonitorIdentity.PnpHardwareKeyFromInstanceName(string.Empty));
Assert.AreEqual(string.Empty, MonitorIdentity.PnpHardwareKeyFromInstanceName(@"DISPLAY"));
Assert.AreEqual(string.Empty, MonitorIdentity.PnpHardwareKeyFromInstanceName(@"DISPLAY\BOE0900"));
Assert.AreEqual(string.Empty, MonitorIdentity.FromInstanceName(null));
Assert.AreEqual(string.Empty, MonitorIdentity.FromInstanceName(string.Empty));
Assert.AreEqual(string.Empty, MonitorIdentity.FromInstanceName(@"DISPLAY"));
Assert.AreEqual(string.Empty, MonitorIdentity.FromInstanceName(@"DISPLAY\BOE0900"));
}
[TestMethod]

View File

@@ -96,6 +96,27 @@ public class MonitorSettingsRebuilderTests
Assert.AreEqual(Now, result[0].LastSeenUtc);
}
[TestMethod]
public void Rebuild_TreatsIdsDifferingOnlyByCase_AsSameMonitor()
{
// Same physical monitor: discovered now spelled upper-case, previously saved lower-case.
// The DevicePath-based Id must be matched case-insensitively so the saved entry is deduped
// against the freshly-discovered one rather than lingering as a stale duplicate.
var current = new List<MonitorInfo>
{
new() { Id = @"\\?\DISPLAY#BOE0900#4&ABC&0&UID111", EnableInputSource = true },
};
var existing = new List<MonitorInfo>
{
Existing(@"\\?\display#boe0900#4&abc&0&uid111", enableInputSource: true, Now.AddDays(-5)),
};
var result = MonitorSettingsRebuilder.Rebuild(current, existing, new FixedClock(Now), retentionDays: 30);
Assert.AreEqual(1, result.Count, "Ids differing only by case denote the same monitor and must dedupe to one entry");
Assert.AreEqual(@"\\?\DISPLAY#BOE0900#4&ABC&0&UID111", result[0].Id);
}
[TestMethod]
public void Rebuild_DiscoveryRevocationRoundtrip_DoesNotLoseFlags()
{

View File

@@ -14,6 +14,7 @@ using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Common.Utils;
using PowerDisplay.Models;
using static PowerDisplay.Common.Drivers.NativeConstants;
using static PowerDisplay.Common.Drivers.NativeDelegates;
using static PowerDisplay.Common.Drivers.PInvoke;
@@ -149,9 +150,9 @@ namespace PowerDisplay.Common.Drivers.DDC
/// Discovers external DDC/CI-managed monitors. Each enumerated hMonitor runs its own
/// async pipeline (filter → physical-handle retrieval → caps fetch + VCP init); all
/// pipelines run concurrently via Task.WhenAll. Caller (MonitorManager) supplies the
/// pre-filtered external-target list from Phase 0.
/// displays it did not route to WMI — i.e. everything WmiMonitorBrightness did not expose.
/// </summary>
/// <param name="targets">External-only display targets (pre-filtered by MonitorManager Phase 0).</param>
/// <param name="targets">Displays MonitorManager did not claim via WMI (not exposed by WmiMonitorBrightness).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of DDC/CI-managed external monitors.</returns>
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(
@@ -171,19 +172,12 @@ namespace PowerDisplay.Common.Drivers.DDC
return Enumerable.Empty<Monitor>();
}
// Wrap the parallel discovery in a CrashDetectionScope. The scope writes
// discovery.lock on Begin and deletes it on Dispose. If the process is killed
// during capabilities I/O (BSOD, FailFast, TerminateProcess), Dispose never runs
// and the lock survives — next PowerDisplay.exe startup notices it via CrashRecovery.
//
// Scope scope note: the original three-phase design wrapped only Phase 2 (cap-string
// fetch). Main's per-handle pipeline interleaves Phase 1 (GDI/MultiMon enumeration)
// and Phase 3 (VCP init) with the fetch, so we wrap the whole Task.WhenAll. Phase 1
// GDI calls return null on failure (don't throw) and Phase 3 has its own catch-all
// in BuildMonitorFromPhysical, so false-positive quarantine from those paths is not
// observed in practice. Single Begin/Dispose per discovery is also required because
// CrashDetectionScope uses FileMode.CreateNew + FileShare.None and cannot be nested
// across the concurrent per-handle pipelines.
// Wrap the whole parallel discovery in a CrashDetectionScope: it writes discovery.lock
// on Begin and deletes it on Dispose, so if the process is killed during capabilities
// I/O (BSOD, FailFast, TerminateProcess) the surviving lock is picked up by
// CrashRecovery on the next startup. A single Begin/Dispose wraps the entire
// Task.WhenAll because CrashDetectionScope uses FileMode.CreateNew + FileShare.None
// and cannot be nested across the per-handle pipelines.
IReadOnlyList<Monitor>[] results;
CrashDetectionScope? scope;
try
@@ -212,7 +206,7 @@ namespace PowerDisplay.Common.Drivers.DDC
}
var monitors = results.SelectMany(r => r).ToList();
var newHandleMap = new Dictionary<string, IntPtr>();
var newHandleMap = new Dictionary<string, IntPtr>(MonitorIdComparer.Instance);
foreach (var m in monitors)
{
newHandleMap[m.Id] = m.Handle;
@@ -662,8 +656,8 @@ namespace PowerDisplay.Common.Drivers.DDC
if (!targetsByGdi.TryGetValue(gdiName, out var matchingInfos))
{
// GDI name not in the external targets list — either a Phase 0 internal
// panel or a target QueryDisplayConfig didn't enumerate. Skip BEFORE the
// GDI name not in the DDC target list — either a panel already claimed by
// WMI or a target QueryDisplayConfig didn't enumerate. Skip BEFORE the
// expensive GetPhysicalMonitorsFromHMONITOR call.
Logger.LogDebug($"DDC skipping {gdiName}: not in external targets list");
return Array.Empty<Monitor>();

View File

@@ -7,6 +7,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using ManagedCommon;
using PowerDisplay.Models;
using static PowerDisplay.Common.Drivers.PInvoke;
namespace PowerDisplay.Common.Drivers.DDC
@@ -17,7 +18,7 @@ namespace PowerDisplay.Common.Drivers.DDC
public partial class PhysicalMonitorHandleManager : IDisposable
{
// Mapping: monitorId -> physical handle (thread-safe)
private readonly ConcurrentDictionary<string, IntPtr> _monitorIdToHandleMap = new();
private readonly ConcurrentDictionary<string, IntPtr> _monitorIdToHandleMap = new(MonitorIdComparer.Instance);
private readonly object _handleLock = new();
private bool _disposed;

View File

@@ -1,75 +0,0 @@
// 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 PowerDisplay.Common.Drivers
{
/// <summary>
/// Classifies displays as internal (built-in) or external based on the
/// DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY enum returned by QueryDisplayConfig.
/// Pure function helper, no side effects.
/// </summary>
/// <remarks>
/// Reference for the full set of OutputTechnology values:
/// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ne-wingdi-displayconfig_video_output_technology
///
/// Common values seen in the wild:
/// 0 HD15 (VGA) 5 HDMI 10 DISPLAYPORT_EXTERNAL
/// 1 SVIDEO 6 LVDS 11 DISPLAYPORT_EMBEDDED (internal)
/// 2 COMPOSITE_VIDEO 8 D_JPN 12 UDI_EXTERNAL
/// 3 COMPONENT_VIDEO 9 SDI 13 UDI_EMBEDDED (internal)
/// 4 DVI 15 MIRACAST
/// 17 INDIRECT_VIRTUAL
/// 0x80000000 INTERNAL high-bit flag, may be combined with a subtype
/// 0xFFFFFFFF OTHER (signed -1)
/// </remarks>
public static class DisplayClassifier
{
// High-bit flag indicating an internal display (DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL).
private const uint InternalFlag = 0x80000000u;
// Documented "embedded" subtypes that mean internal connection.
private const uint DisplayPortEmbedded = 11u;
private const uint UdiEmbedded = 13u;
/// <summary>
/// Returns true if the given OutputTechnology value indicates an internal display.
/// Conservative rule: a value is internal only when it is either
/// (a) the bare INTERNAL high-bit flag (0x80000000) with no subtype,
/// (b) the INTERNAL flag combined with a documented embedded subtype
/// (DISPLAYPORT_EMBEDDED 11 or UDI_EMBEDDED 13), or
/// (c) one of those embedded subtypes on its own.
/// Any other value — including the INTERNAL flag combined with an
/// undocumented subtype — is treated as external. Misclassifying an
/// external display as internal would silently drop it from DDC/CI
/// discovery (WMI has no fallback), so we err on the side of external.
/// LVDS (6) is intentionally NOT classified as internal — the official docs
/// describe it only as a connector type, not as an internal-display marker.
/// </summary>
public static bool IsInternal(uint outputTechnology)
{
// Pure INTERNAL flag with no underlying value.
if (outputTechnology == InternalFlag)
{
return true;
}
// INTERNAL combined with a known embedded subtype.
if ((outputTechnology & InternalFlag) != 0)
{
var underlying = outputTechnology & ~InternalFlag;
if (underlying == DisplayPortEmbedded || underlying == UdiEmbedded)
{
return true;
}
// INTERNAL combined with unknown/undocumented subtype: treat as external.
return false;
}
// Known embedded subtypes without the INTERNAL flag.
return outputTechnology == DisplayPortEmbedded
|| outputTechnology == UdiEmbedded;
}
}
}

View File

@@ -14,9 +14,8 @@ namespace PowerDisplay.Common.Drivers
/// <summary>
/// Win32 DisplayConfig API wrapper that enumerates all active display paths
/// (QueryDisplayConfig + DisplayConfigGetDeviceInfo) and produces a neutral
/// <see cref="MonitorDisplayInfo"/> inventory used by Phase 0 classification.
/// This layer is independent of DDC/CI and WMI — both downstream controllers
/// consume its output via <see cref="DisplayClassifier"/>.
/// <see cref="MonitorDisplayInfo"/> inventory. This layer is independent of DDC/CI and
/// WMI; <see cref="MonitorManager"/> routes the inventory to both downstream controllers.
/// </summary>
public static class DisplayConfigInventory
{
@@ -68,10 +67,10 @@ namespace PowerDisplay.Common.Drivers
continue;
}
// Get target info (friendly name, device path, output technology)
var (friendlyName, devicePath, outputTechnology) = GetTargetDeviceInfo(path.TargetInfo.AdapterId, path.TargetInfo.Id);
// Get target info (friendly name, device path)
var (friendlyName, devicePath) = GetTargetDeviceInfo(path.TargetInfo.AdapterId, path.TargetInfo.Id);
// Use device path as key - unique per target, supports mirror mode
// Device path is the dictionary key; skip targets that don't have one.
if (string.IsNullOrEmpty(devicePath))
{
continue;
@@ -82,11 +81,7 @@ namespace PowerDisplay.Common.Drivers
DevicePath = devicePath,
GdiDeviceName = gdiDeviceName,
FriendlyName = friendlyName ?? string.Empty,
AdapterId = path.TargetInfo.AdapterId,
TargetId = path.TargetInfo.Id,
MonitorNumber = i + 1, // 1-based, matches Windows Display Settings
OutputTechnology = outputTechnology,
IsInternal = DisplayClassifier.IsInternal(outputTechnology),
};
}
}
@@ -135,9 +130,9 @@ namespace PowerDisplay.Common.Drivers
}
/// <summary>
/// Gets friendly name, device path, and output technology for a monitor target.
/// Gets friendly name and device path for a monitor target.
/// </summary>
private static unsafe (string? FriendlyName, string? DevicePath, uint OutputTechnology) GetTargetDeviceInfo(LUID adapterId, uint targetId)
private static unsafe (string? FriendlyName, string? DevicePath) GetTargetDeviceInfo(LUID adapterId, uint targetId)
{
try
{
@@ -157,8 +152,7 @@ namespace PowerDisplay.Common.Drivers
{
return (
deviceName.GetMonitorFriendlyDeviceName(),
deviceName.GetMonitorDevicePath(),
deviceName.OutputTechnology);
deviceName.GetMonitorDevicePath());
}
Logger.LogWarning(
@@ -170,7 +164,7 @@ namespace PowerDisplay.Common.Drivers
$"DisplayConfigInventory: GetTargetDeviceInfo exception (adapter.low=0x{adapterId.LowPart:X}, target={targetId}): {ex.Message}");
}
return (null, null, 0u);
return (null, null);
}
}
}

View File

@@ -2,15 +2,12 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Win32.Foundation;
namespace PowerDisplay.Common.Drivers
{
/// <summary>
/// Monitor display information structure produced by QueryDisplayConfig.
/// Used by MonitorManager Phase 0 classification and by both controllers
/// during discovery. Immutable value type — populated once by
/// <see cref="DisplayConfigInventory"/> and read-only thereafter.
/// Used by MonitorManager during discovery and by both controllers. Immutable value
/// type — populated once by <see cref="DisplayConfigInventory"/> and read-only thereafter.
/// </summary>
public readonly record struct MonitorDisplayInfo
{
@@ -32,27 +29,11 @@ namespace PowerDisplay.Common.Drivers
/// </summary>
public string FriendlyName { get; init; }
public LUID AdapterId { get; init; }
public uint TargetId { get; init; }
/// <summary>
/// Gets the monitor number based on QueryDisplayConfig path index.
/// This matches the number shown in Windows Display Settings "Identify" feature.
/// 1-based index (paths[0] = 1, paths[1] = 2, etc.)
/// </summary>
public int MonitorNumber { get; init; }
/// <summary>
/// Gets the raw DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY value reported
/// by QueryDisplayConfig. Preserved for diagnostic logging.
/// </summary>
public uint OutputTechnology { get; init; }
/// <summary>
/// Gets a value indicating whether this display is classified as internal (built-in).
/// Computed from OutputTechnology by DisplayClassifier.IsInternal during Phase 0.
/// </summary>
public bool IsInternal { get; init; }
}
}

View File

@@ -12,6 +12,7 @@ using ManagedCommon;
using PowerDisplay.Common.Drivers;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Models;
using WmiLight;
using Monitor = PowerDisplay.Common.Models.Monitor;
@@ -192,25 +193,22 @@ namespace PowerDisplay.Common.Drivers.WMI
}
/// <summary>
/// Discover supported monitors.
/// WMI brightness control is typically only available on internal laptop displays.
/// The monitor Name is left blank here; the ViewModel layer fills in a localized
/// "Built-in Display" string so it can be translated for the user's UI language.
/// Discover the panels the WMI brightness provider exposes, pairing each against the
/// active-display inventory. A display present in <c>WmiMonitorBrightness</c> is treated
/// as an internal panel by <see cref="MonitorManager"/>, regardless of the OutputTechnology
/// the active GPU reports — this is what lets a built-in panel driven by the discrete GPU
/// (reported as DisplayPort-External) still be found. See issue #48587.
/// </summary>
/// <param name="targets">Internal-only display targets (pre-filtered by MonitorManager Phase 0).</param>
/// <param name="targets">The full active-display inventory from QueryDisplayConfig.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of WMI-managed internal monitors.</returns>
/// <returns>WMI-managed monitors (those present in both WMI and the inventory).</returns>
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(
IReadOnlyList<MonitorDisplayInfo> targets,
CancellationToken cancellationToken = default)
{
// Short-circuit: with no internal displays classified there is nothing for WMI
// brightness control to do. Skipping the query also avoids the WmiMonitorBrightness
// class throwing WMI 0x1068 ("feature not supported") on systems without an
// internal panel — that exception is otherwise caught and logged as Error.
// No active displays at all — nothing to pair WMI brightness instances against.
if (targets.Count == 0)
{
Logger.LogInfo("WMI: No internal displays classified — skipping WmiMonitorBrightness query");
return Enumerable.Empty<Monitor>();
}
@@ -219,32 +217,25 @@ namespace PowerDisplay.Common.Drivers.WMI
{
var monitors = new List<Monitor>();
// Build PnP-hardware-key -> MonitorDisplayInfo lookup. The PnP key (manufacturer
// code + PnP instance UID) is globally unique per physical device and present in
// both WMI InstanceName and DevicePath, so this is a one-step exact match that
// handles dual-internal-panel devices (e.g. Yoga Book 9i, Zenbook Duo) without
// needing any disambiguation pass.
var monitorDisplayInfos = targets
.Select(t => (Key: MonitorIdentity.PnpHardwareKeyFromDevicePath(t.DevicePath), Info: t))
.Where(p => !string.IsNullOrEmpty(p.Key))
.ToDictionary(p => p.Key, p => p.Info, StringComparer.OrdinalIgnoreCase);
// Track which internal targets (keyed by DevicePath, the unique target id) were
// observed via WmiMonitorBrightness so we can warn about any that were classified
// internal but not exposed.
var seenDevicePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Key the inventory by canonical Monitor.Id (FromDevicePath). A WMI InstanceName
// reduces to the same Id via FromInstanceName, so pairing is a single exact lookup
// that also disambiguates dual-internal-panel devices without a separate pass.
var byId = targets
.Select(t => (Id: MonitorIdentity.FromDevicePath(t.DevicePath), Info: t))
.Where(p => !string.IsNullOrEmpty(p.Id))
.ToDictionary(p => p.Id, p => p.Info, MonitorIdComparer.Instance);
try
{
using var connection = new WmiConnection(WmiNamespace);
// Query WMI brightness support - only internal displays typically support this
// System-wide query: returns every panel the driver exposes for WMI
// brightness, regardless of which GPU currently drives it.
var brightnessQuery = $"SELECT InstanceName, CurrentBrightness FROM {BrightnessQueryClass}";
var brightnessResults = connection.CreateQuery(brightnessQuery).ToList();
// Create monitor objects for each supported brightness instance.
// Check cancellation per iteration since WMI work inside Task.Run
// doesn't respond to the token after the loop starts.
// Check cancellation per iteration: WMI work inside Task.Run doesn't
// respond to the token once the loop has started.
foreach (var obj in brightnessResults)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -254,39 +245,38 @@ namespace PowerDisplay.Common.Drivers.WMI
var instanceName = obj.GetPropertyValue<string>("InstanceName") ?? string.Empty;
var currentBrightness = obj.GetPropertyValue<byte>("CurrentBrightness");
// Derive the same PnP hardware key from the WMI InstanceName and look up
// the matching MonitorDisplayInfo — exact, unique, no disambiguation needed.
var pnpKey = MonitorIdentity.PnpHardwareKeyFromInstanceName(instanceName);
if (string.IsNullOrEmpty(pnpKey) || !monitorDisplayInfos.TryGetValue(pnpKey, out var displayInfo))
// Pair on the canonical Monitor.Id. A miss means this WMI instance is
// not an active display (e.g. a disconnected panel still cached by the
// provider) — skip it.
var lookupId = MonitorIdentity.FromInstanceName(instanceName);
if (string.IsNullOrEmpty(lookupId) || !byId.TryGetValue(lookupId, out var displayInfo))
{
Logger.LogWarning(
$"WMI returned brightness for instance '{instanceName}' but no matching " +
"QueryDisplayConfig target was found — skipping");
Logger.LogInfo(
$"WMI exposed brightness for instance '{instanceName}' with no matching " +
"active display — skipping");
continue;
}
// DevicePath is guaranteed non-empty here: the PnP-key lookup above
// only succeeds for targets whose key was derived from a populated
// DevicePath.
seenDevicePaths.Add(displayInfo.DevicePath);
string uniqueId = MonitorIdentity.FromDevicePath(displayInfo.DevicePath);
int monitorNumber = displayInfo.MonitorNumber;
string gdiDeviceName = displayInfo.GdiDeviceName ?? string.Empty;
// Derive the Id from the matched entry's DevicePath, not the
// reconstructed lookupId. The persisted Monitor.Id ALWAYS comes from this
// single source (FromDevicePath), so a WMI panel's Id stays byte-identical
// to the DDC route and to prior releases. FromInstanceName is only the
// lookup key; every Id comparison/key elsewhere goes through MonitorIdComparer
// (case-insensitive), so an InstanceName/DevicePath casing difference can
// never orphan per-monitor settings.
// Name is left blank: MonitorViewModel injects a localized
// "Built-in Display" string for internal displays.
var monitor = new Monitor
{
Id = uniqueId,
Id = MonitorIdentity.FromDevicePath(displayInfo.DevicePath),
Name = string.Empty,
CurrentBrightness = currentBrightness,
InstanceName = instanceName,
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.Wmi,
CommunicationMethod = "WMI",
SupportsColorTemperature = false,
MonitorNumber = monitorNumber,
GdiDeviceName = gdiDeviceName,
MonitorNumber = displayInfo.MonitorNumber,
GdiDeviceName = displayInfo.GdiDeviceName ?? string.Empty,
};
monitors.Add(monitor);
@@ -299,28 +289,16 @@ namespace PowerDisplay.Common.Drivers.WMI
}
catch (WmiException ex)
{
Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message} (HResult: 0x{ex.HResult:X})");
// On a system with no WMI-controllable panel the provider may be absent or
// throw 0x1068 ("feature not supported"); those displays are handled by
// DDC/CI instead, so this is informational rather than an error.
Logger.LogInfo($"WMI brightness query unavailable: {ex.Message} (HResult: 0x{ex.HResult:X})");
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message}");
}
// Warn about every internal target the driver didn't expose via WMI.
// DevicePath is per-target unique, so dual-internal-panel devices report each
// missing panel separately.
foreach (var target in targets)
{
if (seenDevicePaths.Contains(target.DevicePath))
{
continue;
}
Logger.LogWarning(
$"Internal display \"{target.FriendlyName}\" ({target.DevicePath}) was classified internal " +
"but is not exposed via WmiMonitorBrightness — driver may not support brightness control");
}
return monitors;
},
cancellationToken);

View File

@@ -36,39 +36,18 @@ public static class MonitorIdentity
}
/// <summary>
/// Extract the PnP hardware key from a DevicePath. The key identifies a physical
/// monitor across both QueryDisplayConfig (DevicePath) and WMI (InstanceName)
/// representations, so it is the right join key for pairing WMI brightness instances
/// with MonitorDisplayInfo entries.
/// Convert a WMI <c>WmiMonitorBrightness.InstanceName</c> (e.g.,
/// "DISPLAY\BOE0900\4&amp;...&amp;UID111_0") into the same canonical
/// <see cref="Monitor.Id"/> that <see cref="FromDevicePath"/> produces from the
/// matching QueryDisplayConfig DevicePath for the same physical monitor — e.g.,
/// "\\?\DISPLAY#BOE0900#4&amp;...&amp;UID111". Deriving the Id from each source and
/// comparing is the join key for pairing WMI brightness instances with
/// <c>MonitorDisplayInfo</c> entries; it stays unique even on dual-internal-panel
/// devices (Yoga Book 9i, Zenbook Duo) where two panels share an EdidId but differ
/// in PnP UID.
/// </summary>
/// <param name="devicePath">DevicePath of the form "\\?\DISPLAY#BOE0900#4&amp;...&amp;UID111#{guid}".</param>
/// <returns>Canonical key "BOE0900#4&amp;...&amp;UID111", or empty string if extraction fails.</returns>
public static string PnpHardwareKeyFromDevicePath(string? devicePath)
{
if (string.IsNullOrEmpty(devicePath))
{
return string.Empty;
}
// Split: ["\\?\DISPLAY", "BOE0900", "4&...&UID111", "{guid}"]
var parts = devicePath.Split('#');
if (parts.Length < 3 || string.IsNullOrEmpty(parts[1]) || string.IsNullOrEmpty(parts[2]))
{
return string.Empty;
}
return $"{parts[1]}#{parts[2]}";
}
/// <summary>
/// Extract the PnP hardware key from a WMI InstanceName. Produces the same canonical
/// form as <see cref="PnpHardwareKeyFromDevicePath"/> for the same physical device,
/// enabling reliable one-step matching even on dual-internal-panel devices where
/// two panels share an EdidId but differ in PnP UID.
/// </summary>
/// <param name="instanceName">InstanceName of the form "DISPLAY\BOE0900\4&amp;...&amp;UID111_0".</param>
/// <returns>Canonical key "BOE0900#4&amp;...&amp;UID111", or empty string if extraction fails.</returns>
public static string PnpHardwareKeyFromInstanceName(string? instanceName)
/// <param name="instanceName">InstanceName of the form "DISPLAY\&lt;EdidId&gt;\&lt;instance&gt;_&lt;N&gt;". Null, empty, or not a three-segment InstanceName returns empty string.</param>
public static string FromInstanceName(string? instanceName)
{
if (string.IsNullOrEmpty(instanceName))
{
@@ -82,7 +61,7 @@ public static class MonitorIdentity
return string.Empty;
}
// Strip the trailing "_N" WMI-instance suffix (e.g. "..._0").
// Strip the trailing "_N" WMI-instance suffix (e.g. "..._0", "..._12").
var instanceSegment = parts[2];
var underscore = instanceSegment.LastIndexOf('_');
if (underscore > 0)
@@ -90,7 +69,11 @@ public static class MonitorIdentity
instanceSegment = instanceSegment[..underscore];
}
return $"{parts[1]}#{instanceSegment}";
// Reshape into the canonical DevicePath-style Monitor.Id: a WMI InstanceName uses
// "\" separators and omits the "\\?\" device-interface prefix, whereas a
// QueryDisplayConfig DevicePath uses "#" separators with that prefix. parts[0] is
// the enumerator ("DISPLAY"), reused rather than hardcoded.
return $@"\\?\{parts[0]}#{parts[1]}#{instanceSegment}";
}
/// <summary>

View File

@@ -56,7 +56,7 @@ public static class MonitorSettingsRebuilder
continue;
}
if (result.Any(m => m.Id == existingMonitor.Id))
if (result.Any(m => MonitorIdComparer.Equal(m.Id, existingMonitor.Id)))
{
continue;
}

View File

@@ -13,6 +13,7 @@ using ManagedCommon;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Serialization;
using PowerDisplay.Common.Utils;
using PowerDisplay.Models;
namespace PowerDisplay.Common.Services
{
@@ -25,7 +26,7 @@ namespace PowerDisplay.Common.Services
public partial class MonitorStateManager : IDisposable
{
private readonly string _stateFilePath;
private readonly ConcurrentDictionary<string, MonitorState> _states = new();
private readonly ConcurrentDictionary<string, MonitorState> _states = new(MonitorIdComparer.Instance);
private readonly SimpleDebouncer _saveDebouncer;
private volatile bool _disposed;

View File

@@ -445,7 +445,7 @@ namespace PowerDisplay.Common.Utils
var custom = customMappings.FirstOrDefault(m =>
m.VcpCode == vcpCode &&
m.Value == value &&
(m.ApplyToAll || (!m.ApplyToAll && m.TargetMonitorId == monitorId)));
(m.ApplyToAll || (!m.ApplyToAll && MonitorIdComparer.Equal(m.TargetMonitorId, monitorId))));
if (custom != null && !string.IsNullOrEmpty(custom.CustomName))
{

View File

@@ -0,0 +1,47 @@
// 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;
namespace PowerDisplay.Models;
/// <summary>
/// The single canonical equality policy for a monitor's stable Id (the DevicePath-based
/// <c>"\\?\DISPLAY#&lt;EdidId&gt;#&lt;instance&gt;"</c> string). Every dictionary, hash set,
/// and equality check keyed on a monitor Id MUST go through this type so the policy lives in
/// exactly one place.
/// </summary>
/// <remarks>
/// <para>
/// Ordinal and case-<b>insensitive</b>. A persisted Id is normally re-derived from the
/// QueryDisplayConfig DevicePath (<c>MonitorIdentity.FromDevicePath</c>), so the same physical
/// monitor reproduces a byte-identical Id across runs and case-sensitive matching happens to
/// work today. But the WMI brightness <c>InstanceName</c> and the DevicePath for the same panel
/// can differ in casing (already handled case-insensitively where they are joined), and the
/// DevicePath casing is not guaranteed stable across driver updates or GPU-route changes. To
/// avoid orphaning per-monitor settings on a mere casing change — and to keep one consistent
/// rule across the in-memory join and the persisted stores — Id casing is treated as
/// non-significant everywhere.
/// </para>
/// <para>
/// Lives in <c>PowerDisplay.Models</c> because it is the only project referenced by both the
/// discovery/persistence code (<c>PowerDisplay.Lib</c>) and the settings library
/// (<c>Settings.UI.Library</c> / <c>Settings.UI</c>) that key collections on a monitor Id.
/// </para>
/// </remarks>
public static class MonitorIdComparer
{
/// <summary>
/// Canonical comparer for monitor-Id-keyed <see cref="System.Collections.Generic.Dictionary{TKey,TValue}"/>,
/// <see cref="System.Collections.Generic.HashSet{T}"/>, and LINQ lookups.
/// </summary>
public static readonly StringComparer Instance = StringComparer.OrdinalIgnoreCase;
/// <summary>
/// Returns <see langword="true"/> when two monitor Ids denote the same monitor under the
/// canonical policy. Use in place of the <c>==</c> operator when comparing monitor Ids.
/// </summary>
public static bool Equal(string? left, string? right)
=> string.Equals(left, right, StringComparison.OrdinalIgnoreCase);
}

View File

@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -16,6 +15,7 @@ using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Common.Utils;
using PowerDisplay.Models;
using Monitor = PowerDisplay.Common.Models.Monitor;
namespace PowerDisplay.Helpers
@@ -27,7 +27,7 @@ namespace PowerDisplay.Helpers
public partial class MonitorManager : IDisposable
{
private readonly List<Monitor> _monitors = new();
private readonly Dictionary<string, Monitor> _monitorLookup = new();
private readonly Dictionary<string, Monitor> _monitorLookup = new(MonitorIdComparer.Instance);
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
private readonly DisplayRotationService _rotationService = new();
@@ -123,18 +123,19 @@ namespace PowerDisplay.Helpers
}
/// <summary>
/// Classify all displays via OutputTechnology using a single QueryDisplayConfig
/// call, then dispatch strictly-scoped target lists to each controller in parallel
/// (WMI = internal only, DDC/CI = external only).
/// Discover monitors by capability, not by nominal output technology. WMI runs first
/// over the full QueryDisplayConfig inventory; every display it claims is a
/// WMI-controllable internal panel. Whatever WMI does not claim is then sent to DDC/CI.
/// This avoids incorrectly routing a built-in panel that the active (discrete) GPU reports as
/// DisplayPort-External — the root cause of issue #48587.
/// </summary>
private async Task<List<Monitor>> DiscoverFromAllControllersAsync(CancellationToken cancellationToken)
{
var inventory = DisplayConfigInventory.GetAllMonitorDisplayInfo();
// Filter blacklisted monitors out of the inventory before any controller
// is dispatched. Matching uses MonitorIdentity.EdidIdFromMonitorId on each
// entry's DevicePath, so blocked monitors are not opened, probed, or queried
// — the whole point of the blacklist over the per-monitor IsHidden flag.
// Filter blacklisted monitors before any controller runs, so blocked displays are
// never opened, probed, or queried (unlike the per-monitor IsHidden flag). Matching
// is by MonitorIdentity.EdidIdFromMonitorId on each entry's DevicePath.
var beforeCount = inventory.Count;
var filteredInventory = new Dictionary<string, MonitorDisplayInfo>(
inventory.Count, StringComparer.OrdinalIgnoreCase);
@@ -165,59 +166,61 @@ namespace PowerDisplay.Helpers
return new List<Monitor>();
}
var byKind = inventory.Values.ToLookup(i => i.IsInternal);
IReadOnlyList<MonitorDisplayInfo> internalTargets = byKind[true].ToList();
IReadOnlyList<MonitorDisplayInfo> externalTargets = byKind[false].ToList();
var allDisplays = inventory.Values.ToList();
LogClassificationSummary(internalTargets, externalTargets);
// Phase 1: WMI over the full inventory — whatever it claims is an internal panel.
var wmiMonitors = _wmiController != null
? (await SafeDiscoverAsync(_wmiController, allDisplays, cancellationToken)).ToList()
: new List<Monitor>();
var tasks = new List<Task<IEnumerable<Monitor>>>();
var wmiClaimedIds = new HashSet<string>(
wmiMonitors.Select(m => m.Id), MonitorIdComparer.Instance);
if (_ddcController != null)
{
tasks.Add(SafeDiscoverAsync(_ddcController, externalTargets, cancellationToken));
}
// Phase 2: everything WMI did not claim goes to DDC/CI. Accepted trade-off — a
// monitor exposing both is controlled via WMI only and won't get DDC-only features
// (contrast/volume/input). Partition once so FromDevicePath runs a single time each.
var byRoute = allDisplays.ToLookup(
d => wmiClaimedIds.Contains(MonitorIdentity.FromDevicePath(d.DevicePath)));
IReadOnlyList<MonitorDisplayInfo> wmiTargets = byRoute[true].ToList();
IReadOnlyList<MonitorDisplayInfo> ddcTargets = byRoute[false].ToList();
if (_wmiController != null)
{
tasks.Add(SafeDiscoverAsync(_wmiController, internalTargets, cancellationToken));
}
LogClassificationSummary(wmiTargets, ddcTargets);
var results = await Task.WhenAll(tasks);
return results.SelectMany(m => m).ToList();
var ddcMonitors = _ddcController != null
? (await SafeDiscoverAsync(_ddcController, ddcTargets, cancellationToken)).ToList()
: new List<Monitor>();
return wmiMonitors.Concat(ddcMonitors).ToList();
}
/// <summary>
/// Logs the result of Phase 0 classification at Info level, one line per display
/// plus a summary. Used for diagnostic traceability of internal/external decisions.
/// Logs how each display was routed (WMI vs DDC/CI) at Info level, one line per
/// display plus a summary. Runs after WMI discovery but before the crash-prone DDC/CI
/// capability fetch, so every attached model's EdidId is on disk for crash correlation.
/// </summary>
private static void LogClassificationSummary(
IReadOnlyList<MonitorDisplayInfo> internalTargets,
IReadOnlyList<MonitorDisplayInfo> externalTargets)
IReadOnlyList<MonitorDisplayInfo> wmiTargets,
IReadOnlyList<MonitorDisplayInfo> ddcTargets)
{
Logger.LogInfo($"[DisplayClassification] Found {internalTargets.Count + externalTargets.Count} displays:");
Logger.LogInfo($"[DisplayClassification] Found {wmiTargets.Count + ddcTargets.Count} displays:");
foreach (var info in internalTargets.Concat(externalTargets).OrderBy(i => i.MonitorNumber))
var wmiPaths = new HashSet<string>(wmiTargets.Select(t => t.DevicePath), StringComparer.OrdinalIgnoreCase);
foreach (var info in wmiTargets.Concat(ddcTargets).OrderBy(i => i.MonitorNumber))
{
var techValue = info.OutputTechnology >= 0x80000000u
? "0x" + info.OutputTechnology.ToString("X", CultureInfo.InvariantCulture)
: info.OutputTechnology.ToString(CultureInfo.InvariantCulture);
var classification = info.IsInternal ? "Internal" : "External";
var route = wmiPaths.Contains(info.DevicePath) ? "WMI (internal)" : "DDC/CI (external)";
// Log EdidId (manufacturer+product code from EDID) up front, before any
// DDC/CI capability fetch runs. QueryDisplayConfig reads OS-cached EDID and
// cannot BSOD, so this line is guaranteed on disk before the crash-prone
// Phase 2 fetch starts — recovered logs identify every attached model
// (and same-model duplicates) for crash correlation.
// EdidId (manufacturer+product code) is logged here, before the BSOD-prone DDC
// capability fetch, so recovered logs identify every attached model (and
// same-model duplicates) for crash correlation.
var edidId = MonitorIdentity.EdidIdFromMonitorId(info.DevicePath);
var edidIdField = string.IsNullOrEmpty(edidId) ? "?" : edidId;
Logger.LogInfo(
$" [Path {info.MonitorNumber}] EdidId={edidIdField} {info.GdiDeviceName} / \"{info.FriendlyName}\": " +
$"OutputTechnology={techValue} → {classification}");
$" [Path {info.MonitorNumber}] EdidId={edidIdField} {info.GdiDeviceName} / \"{info.FriendlyName}\" → {route}");
}
Logger.LogInfo($"[DisplayClassification] Summary: {internalTargets.Count} internal, {externalTargets.Count} external");
Logger.LogInfo($"[DisplayClassification] Summary: {wmiTargets.Count} WMI, {ddcTargets.Count} DDC/CI");
}
/// <summary>

View File

@@ -11,6 +11,7 @@ using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerDisplay.Common.Models;
using PowerDisplay.Helpers;
using PowerDisplay.Models;
using Monitor = PowerDisplay.Common.Models.Monitor;
namespace PowerDisplay.ViewModels;
@@ -183,5 +184,6 @@ public partial class MainViewModel
=> new HashSet<string>(
settings.Properties.Monitors
.Where(m => m.IsHidden)
.Select(m => m.Id));
.Select(m => m.Id),
MonitorIdComparer.Instance);
}

View File

@@ -217,7 +217,7 @@ public partial class MainViewModel
foreach (var setting in monitorSettings)
{
// Find monitor by Id (unique identifier)
var monitorVm = Monitors.FirstOrDefault(m => m.Id == setting.MonitorId);
var monitorVm = Monitors.FirstOrDefault(m => MonitorIdComparer.Equal(m.Id, setting.MonitorId));
if (monitorVm == null)
{
@@ -293,7 +293,7 @@ public partial class MainViewModel
private void ApplyFeatureVisibility(MonitorViewModel monitorVm, PowerDisplaySettings settings)
{
var monitorSettings = settings.Properties.Monitors.FirstOrDefault(m =>
m.Id == monitorVm.Id);
MonitorIdComparer.Equal(m.Id, monitorVm.Id));
if (monitorSettings != null)
{
@@ -340,8 +340,8 @@ public partial class MainViewModel
// Filter out monitors with empty IDs to avoid dictionary key collision errors
var existingMonitorSettings = settings.Properties.Monitors
.Where(m => !string.IsNullOrEmpty(m.Id))
.GroupBy(m => m.Id)
.ToDictionary(g => g.Key, g => g.First());
.GroupBy(m => m.Id, MonitorIdComparer.Instance)
.ToDictionary(g => g.Key, g => g.First(), MonitorIdComparer.Instance);
// Build monitor list using Settings UI's MonitorInfo model
// Only include monitors with valid (non-empty) IDs to auto-fix corrupted settings
@@ -394,7 +394,7 @@ public partial class MainViewModel
continue;
}
var target = monitors.FirstOrDefault(m => m.Id == newId);
var target = monitors.FirstOrDefault(m => MonitorIdComparer.Equal(m.Id, newId));
if (target != null)
{
CopyUserFlags(target, legacy);
@@ -566,7 +566,7 @@ public partial class MainViewModel
.ToList())
{
var newId = MonitorIdMigrator.MatchNewId(legacy.MonitorId, discovered);
if (newId != null && profile.MonitorSettings.All(s => s.MonitorId != newId))
if (newId != null && profile.MonitorSettings.All(s => !MonitorIdComparer.Equal(s.MonitorId, newId)))
{
profile.MonitorSettings.Add(new ProfileMonitorSetting(
newId,

View File

@@ -681,8 +681,8 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
}
/// <summary>
/// Set power state for this monitor.
/// Note: Setting any state other than "On" will turn off the display.
/// Set the monitor's power state via VCP 0xD6: On (0x01) wakes the display,
/// Standby/Suspend/Off put it to sleep.
/// </summary>
public async Task SetPowerStateAsync(int powerState)
{
@@ -712,18 +712,6 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
}
}
/// <summary>
/// Command to set power state
/// </summary>
[RelayCommand]
private async Task SetPowerState(int? state)
{
if (state.HasValue)
{
await SetPowerStateAsync(state.Value);
}
}
public int Contrast
{
get => _contrast;
@@ -880,11 +868,9 @@ public partial class MonitorViewModel : ObservableObject, IDisposable
return;
}
if (item.Value == PowerStateItem.PowerStateOn)
{
return;
}
// Send the selected state straight to the hardware. Selecting On (0x01) wakes a
// sleeping monitor: DDC/CI stays reachable in Standby/Suspend/Off(DPM), so the
// write turns the panel back on (Off(Hard)/0x05 may still need a physical wake).
await SetPowerStateAsync(item.Value);
}

View File

@@ -12,11 +12,6 @@ namespace PowerDisplay.ViewModels;
/// </summary>
public class PowerStateItem
{
/// <summary>
/// VCP power mode value representing On state
/// </summary>
public const int PowerStateOn = 0x01;
/// <summary>
/// VCP value for this power state
/// </summary>

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.UI.Xaml;
namespace Microsoft.PowerToys.QuickAccess;
@@ -14,14 +15,26 @@ public partial class App : Application
public App()
{
InitializeComponent();
UnhandledException += App_UnhandledException;
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
var launchContext = QuickAccessLaunchContext.Parse(Environment.GetCommandLineArgs());
_window = new MainWindow(launchContext);
_window.Closed += OnWindowClosed;
_window.Activate();
try
{
var launchContext = QuickAccessLaunchContext.Parse(Environment.GetCommandLineArgs());
_window = new MainWindow(launchContext);
_window.Closed += OnWindowClosed;
_window.Activate();
}
catch (Exception ex)
{
// Failing here means the flyout host could not be constructed. Log and exit cleanly
// rather than letting the throw bubble out into a stowed XAML failure that crashes
// the runner-owned launcher.
Logger.LogError("QuickAccess: failed to launch flyout host.", ex);
Exit();
}
}
private static void OnWindowClosed(object sender, WindowEventArgs args)
@@ -33,4 +46,13 @@ public partial class App : Application
_window = null;
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// QuickAccess is a transient launcher flyout owned by the runner. An unhandled XAML
// exception here would otherwise be stowed and FailFast the process; mark the event
// handled so the next summon can recover. The error is still recorded for diagnostics.
Logger.LogError("QuickAccess: unhandled XAML exception.", e.Exception);
e.Handled = true;
}
}

View File

@@ -2,11 +2,13 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.PowerToys.QuickAccess.Services;
using Microsoft.PowerToys.QuickAccess.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.QuickAccess.Flyout;
@@ -22,6 +24,7 @@ public sealed partial class ShellPage : Page
public ShellPage()
{
InitializeComponent();
ContentFrame.NavigationFailed += ContentFrame_NavigationFailed;
}
public void Initialize(IQuickAccessCoordinator coordinator, LauncherViewModel launcherViewModel, AllAppsViewModel allAppsViewModel)
@@ -65,4 +68,13 @@ public sealed partial class ShellPage : Page
appsListPage.ViewModel?.RefreshSettings();
}
}
private static void ContentFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
// A page constructor or XAML load failure here would otherwise bubble out of the
// Frame and crash the launcher. Log the failure and mark it handled so the flyout
// can remain available; the next summon will retry navigation.
Logger.LogError($"QuickAccess: navigation to '{e.SourcePageType?.FullName}' failed.", e.Exception);
e.Handled = true;
}
}

View File

@@ -675,51 +675,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return string.Join(System.Environment.NewLine, lines);
}
public string GetDiagnosticsAsText()
{
var lines = new List<string>();
lines.Add("Power Display monitor diagnostics");
lines.Add($"Generated: {DateTimeOffset.Now:u}");
lines.Add(string.Empty);
lines.Add("Monitor");
lines.Add(new string('-', 50));
lines.Add($"Name: {Name}");
lines.Add($"Display name: {DisplayName}");
lines.Add($"Monitor number: {MonitorNumber}");
lines.Add($"Monitor ID: {Id}");
lines.Add($"Communication method: {CommunicationMethod}");
lines.Add(string.Empty);
lines.Add("Current values");
lines.Add(new string('-', 50));
lines.Add($"Brightness: {CurrentBrightness}");
lines.Add($"Contrast: {Contrast}");
lines.Add($"Volume: {Volume}");
lines.Add($"Color temperature VCP: 0x{ColorTemperatureVcp:X2}");
lines.Add(string.Empty);
lines.Add("Detected support");
lines.Add(new string('-', 50));
lines.Add($"Supports brightness: {SupportsBrightness}");
lines.Add($"Supports contrast: {SupportsContrast}");
lines.Add($"Supports volume: {SupportsVolume}");
lines.Add($"Supports input source: {SupportsInputSource}");
lines.Add($"Supports color temperature: {SupportsColorTemperature}");
lines.Add($"Supports power state: {SupportsPowerState}");
lines.Add(string.Empty);
lines.Add("Raw capabilities");
lines.Add(new string('-', 50));
lines.Add(string.IsNullOrWhiteSpace(CapabilitiesRaw) ? "No raw capabilities detected" : CapabilitiesRaw);
lines.Add(string.Empty);
lines.Add(GetVcpCodesAsText());
return string.Join(Environment.NewLine, lines);
}
/// <summary>
/// Update this monitor's properties from another MonitorInfo instance.
/// This preserves the object reference while updating all properties.

View File

@@ -62,6 +62,7 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<controls:SettingsGroup x:Uid="PowerDisplayGroupHeader" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander
x:Uid="PowerDisplayFlyoutHeader"
@@ -263,17 +264,6 @@
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="PowerDisplay_Monitor_HideMonitor" IsChecked="{x:Bind IsHidden, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Diagnostics">
<Button
x:Uid="PowerDisplay_Monitor_Diagnostics_CopyButton"
Click="CopyMonitorDiagnostics_Click"
Tag="{x:Bind}">
<StackPanel Orientation="Horizontal" Spacing="{StaticResource PowerDisplayInlineIconSpacing}">
<FontIcon FontSize="{StaticResource PowerDisplayPrimaryGlyphFontSize}" Glyph="&#xE8C8;" />
<TextBlock x:Uid="PowerDisplay_Monitor_Diagnostics_CopyText" />
</StackPanel>
</Button>
</tkcontrols:SettingsCard>
<!-- VCP Capabilities -->
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_VcpCapabilities" Visibility="{x:Bind HasCapabilities, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
@@ -359,4 +349,4 @@
<controls:PageLink x:Uid="LearnMore_PowerDisplay" Link="https://aka.ms/PowerToysOverview_PowerDisplay" />
</controls:SettingsPageControl.PrimaryLinks>
</controls:SettingsPageControl>
</local:NavigablePage>
</local:NavigablePage>

View File

@@ -57,18 +57,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
}
private void CopyMonitorDiagnostics_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is MonitorInfo monitor)
{
var diagnosticsText = monitor.GetDiagnosticsAsText();
var dataPackage = new DataPackage();
dataPackage.SetText(diagnosticsText);
Clipboard.SetContent(dataPackage);
}
}
// Profile button event handlers
private void ProfileButton_Click(object sender, RoutedEventArgs e)
{

View File

@@ -5265,15 +5265,6 @@ The break timer font matches the text font.</value>
<data name="LightSwitch_PowerDisplayDisabledWarningBar.Message" xml:space="preserve">
<value />
</data>
<data name="PowerDisplay_Monitor_Diagnostics.Header" xml:space="preserve">
<value>Monitor diagnostics</value>
</data>
<data name="PowerDisplay_Monitor_Diagnostics.Description" xml:space="preserve">
<value>Copy monitor details, detected capabilities, and VCP codes to the clipboard for troubleshooting.</value>
</data>
<data name="PowerDisplay_Monitor_Diagnostics_CopyText.Text" xml:space="preserve">
<value>Copy diagnostics</value>
</data>
<data name="LightSwitch_ApplyMonitorSettingsExpander.Header" xml:space="preserve">
<value>Apply monitor settings to</value>
</data>

View File

@@ -639,7 +639,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
else
{
// Create a dictionary for quick lookup by Id
var updatedMonitorsDict = updatedMonitors.ToDictionary(m => m.Id, m => m);
var updatedMonitorsDict = updatedMonitors.ToDictionary(m => m.Id, m => m, MonitorIdComparer.Instance);
// Update existing monitors or remove ones that no longer exist
for (int i = Monitors.Count - 1; i >= 0; i--)