Compare commits

...

16 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
63 changed files with 1464 additions and 635 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

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