Compare commits

..

21 Commits

Author SHA1 Message Date
Jessica Dene Earley-Cha
97f2868481 Fix: Narrator announces checkbox labels in CmdPal Extensions > Installed Apps page (#48135)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

Fixes labels not associated with controls in Installed Apps pane.
Narrator only announces "space, checkbox, checked/unchecked" for
Extensions > Installed Apps page checkboxes instead of reading the label
too.

this is an a11y internal bug

The Adaptive Cards renderer (`AdaptiveCards.Rendering.WinUI3` v2.x)
renders `Input.Toggle` as a CheckBox whose `Content` is a TextBlock
containing just `" "`. Without an AutomationProperties.Name`, Narrator
reads the CheckBox.Content (a space character), making the settings
inaccessible to screen reader users.

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

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

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

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

---------

Co-authored-by: Michael Jolley <mike@baldbeardedbuilder.com>
2026-06-03 07:50:35 +02:00
Pranshu Namdeo
f3d3abc552 Fix Performance Monitor settings file path collision (#48251)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Fixes #48224

The Performance Monitor extension was still storing its settings in the
shared settings.json file. Since Command Palette built-in extensions now
use extension-specific sibling settings files, the extension's settings
could be overwritten when Command Palette personalization settings were
saved.

This change updates SettingsJsonPath() to store Performance Monitor
settings in an extension-specific settings file
(performanceMonitor.settings.json), ensuring the network speed unit
setting persists correctly.

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

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

The Performance Monitor extension had not been migrated to the newer
extension-specific settings file architecture. As a result, its settings
were stored in the shared settings.json file and could be lost when the
Command Palette host rewrote its configuration.

Following @zadjii-msft's guidance in the issue, this PR updates
SettingsManager.cs to store Performance Monitor settings in an
extension-specific settings file:

performanceMonitor.settings.json

instead of the shared:

settings.json

This aligns the extension with the current Command Palette settings
architecture and prevents the network speed unit setting from being
overwritten.


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

* Changed Network Speed Unit to "Binary bytes per second" in Performance
Monitor settings.
* Verified the setting was successfully saved to the newly created
`performanceMonitor.settings.json` file.
* Modified core Command Palette personalization settings.
* Verified the Performance Monitor setting was preserved and not
overwritten.
* Restarted PowerToys and verified the setting persisted correctly.

**Note to reviewers:** I left the "Tests" box unchecked because this is
a single-line file path configuration change. I did not add an automated
test, but I have thoroughly verified the fix manually as described in
the Validation steps above.
2026-06-02 16:33:17 +00:00
Michael Jolley
0a64561ed5 CmdPal: Allow connecting to arbitrary hostnames in Remote Desktop list page (#48069)
## Summary of the Pull Request

Fixes #48053 — Remote Desktop extension now allows connecting to
arbitrary hostnames when navigated into the extension's list page.

Previously, `RemoteDesktopListPage` only displayed previously saved
connections. This converts it from `ListPage` to `DynamicListPage`,
adding search-driven behavior:

- Typing a valid hostname/IP that **doesn't** match an existing saved
connection shows a **"Connect to {hostname}"** item at the top of the
list
- Typing something that **exactly matches** a saved connection shows
only the normal results (no duplicate arbitrary-host item)
- Typing an invalid string (not a valid hostname/IP) shows no
arbitrary-host item

The hostname validation reuses the same logic as
`FallbackRemoteDesktopItem`: strip the port suffix (e.g. `myhost:3389` →
`myhost`), then check `Uri.CheckHostName` against `IPv4`, `IPv6`, `Dns`.

## PR Checklist
- [x] Closes: #48053
- [ ] **Communication:** I've discussed this with core contributors
already.
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized

---------

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-02 10:47:25 -05:00
Niels Laute
7f19817182 Rework Power Display warning dialog (#48249)
## Summary of the Pull Request

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

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

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

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

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

## Validation Steps Performed

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-02 14:57:43 +08:00
Niels Laute
a33fd3c474 [KBM] Enable new editor by default (#48245)
## Summary

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

## Behavior

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

## Changes

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

## Validation

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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-02 14:57:02 +08:00
Niels Laute
b712fa4d85 Reword Shortcut Guide module and OOBE descriptions (#48248)
## Summary of the Pull Request

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

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

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

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

No code or behavioral changes.

## Validation Steps Performed

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-02 14:39:52 +08:00
Michael Jolley
b66b044210 CmdPal: Synchronize fallback title/subtitle format for consistent scoring (#48085)
## Summary

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

## Problem

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

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

## Fix

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

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

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

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

## Changes

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

---------

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 16:57:30 -05:00
Michael Jolley
18919eaa40 CmdPal: Fix dock subtitle visibility in compact mode after async update (#48088)
## Summary

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

## Fix

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

Fixes #47980

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 16:56:33 -05:00
Michael Jolley
e0854fbaf3 CmdPal: Reorder dock network stats to match Task Manager order (#48098)
## Summary

Fixes #47939

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

## Changes

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

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 16:55:41 -05:00
Michael Jolley
ba20da1611 CmdPal: Fix hotkey navigation when palette is showing transient dock page (#48089)
## Summary

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

## Root Cause

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

## Fix

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

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

Fixes #47994

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 16:29:53 -05:00
Niels Laute
35f2ed839e CmdPal: Reorder Pin to Dock dialog so controls precede the preview (#48250)
## Summary of the Pull Request

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

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

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

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

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

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

## Validation Steps Performed

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 15:43:46 -05:00
Michael Jolley
e20b5b9c51 CmdPal: Fix GPU index out of range crash in PerfMon widget (#48103)
## Summary

Fixes #47821

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

## Root Cause

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

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

## Changes

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

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 20:59:57 +02:00
Mike Griese
c6a9ad2ad0 cmdpal: fix the dock window border being visible, redux (#48180)
Addenda to #47187

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

Tested by disconnecting and reconnecting RDP a couple times, which
pretty consistently reproduces the problem.
2026-06-01 11:33:17 +00:00
Muyuan Li
a67fc2d9b7 Fix ShortcutGuide v2 crash when Manifests directory is missing (#48171)
## Summary

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

### Root Cause

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

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

### Fix (3 layers)

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

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

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 16:28:08 +08:00
Jessica Dene Earley-Cha
c78f6e52a0 [CmdPal] Toggle "Show details" / "Hide details" with icon in context menu (#48140)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

Address internal a11y bug.



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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-06-01 13:19:38 +08:00
thetsaw
9a55209d13 Add DiskAnalyzer to third-party Run plugins list (#48106)
## Summary of the Pull Request

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

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

## PR Checklist

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

This is a documentation-only change adding one row to the third-party
plugins table. No source code, binaries, or build files are modified.
2026-05-28 17:46:23 +00:00
Copilot
0cb6fe250b Rename Settings UI label from “Shortcut Guide V2” to “Shortcut Guide” (#48151)
## Summary of the Pull Request

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

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

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

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

## Validation Steps Performed

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

---------

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

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


## PR Checklist

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

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

No other modules or strings affected.

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

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:28:31 +00:00
moooyo
cd5027fa1a [PowerDisplay] Fix false-positive crash detection on cooperative shutdown (#48173)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

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

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

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

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

### Root cause

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

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

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

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

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

### Fix

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

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

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

### Testability

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

### Files touched

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

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

### Automated

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

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

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

### Manual

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

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:02:14 +00:00
Copilot
7da62cdb0a Rename OOBE overview Learn link label to “Documentation” (#48155)
## Summary of the Pull Request

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

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

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

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

## Validation Steps Performed

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-28 15:26:02 +08:00
Eymard Silva
c46083dd8d Handle complex calculator results (#47506)
## Summary of the Pull Request

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

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

Fixes #43937

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

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

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

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

## Validation Steps Performed

- Added unit test coverage for `=sqrt(-1)` returning `Complex numbers
are not supported`.
- Added unit test coverage for global query `sqrt(-1)` returning no
result instead of surfacing an unhandled exception.
- Ran `git diff --check`.
- Attempted local build/test with the PowerToys build scripts, but local
validation was blocked by Visual Studio/VC tooling configuration issues
unrelated to this change: `PlatformToolsetVersion` resolves to an empty
value during restore/build.
2026-05-28 15:06:22 +08:00
67 changed files with 1102 additions and 592 deletions

View File

@@ -1970,6 +1970,7 @@ UNORM
unparsable
unremapped
Unsend
Unsubscribes
untriaged
unvirtualized
unwide

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -79,3 +79,4 @@ Below are community created plugins that target a website or software. They are
| [Linear](https://github.com/vednig/powertoys-linear) | [vednig](https://github.com/vednig) | Create Linear Issues directly from Powertoys Run |
| [PerplexitySearchShortcut](https://github.com/0x6f677548/PowerToys-Run-PerplexitySearchShortcut) | [0x6f677548](https://github.com/0x6f677548) | Search Perplexity |
| [SpeedTest](https://github.com/ruslanlap/PowerToysRun-SpeedTest) | [ruslanlap](https://github.com/ruslanlap) | One-command internet speed tests with real-time results, modern UI, and shareable links. |
| [DiskAnalyzer](https://github.com/thetsaw/PowerToys.Plugin) | [thetsaw](https://github.com/thetsaw) | Scan folders, find the largest files, and view drive space usage with visual progress bars. |

View File

@@ -4,15 +4,23 @@
<?define ShortcutGuideAssetsFiles=?>
<?define ShortcutGuideAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\ShortcutGuide\?>
<?define ShortcutGuideManifestsFiles=?>
<?define ShortcutGuideManifestsFilesPath=$(var.BinDir)WinUI3Apps\Assets\ShortcutGuide\Manifests\?>
<Fragment>
<DirectoryRef Id="WinUI3AppsAssetsFolder">
<Directory Id="ShortcutGuideAssetsFolder" Name="ShortcutGuide" />
<Directory Id="ShortcutGuideAssetsFolder" Name="ShortcutGuide">
<Directory Id="ShortcutGuideManifestsFolder" Name="Manifests" />
</Directory>
</DirectoryRef>
<DirectoryRef Id="ShortcutGuideAssetsFolder" FileSource="$(var.ShortcutGuideAssetsFilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--ShortcutGuideAssetsFiles_Component_Def-->
</DirectoryRef>
<DirectoryRef Id="ShortcutGuideManifestsFolder" FileSource="$(var.ShortcutGuideManifestsFilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--ShortcutGuideManifestsFiles_Component_Def-->
</DirectoryRef>
<!-- Shortcut guide -->
<ComponentGroup Id="ShortcutGuideComponentGroup" >
@@ -22,6 +30,12 @@
</RegistryKey>
<RemoveFolder Id="RemoveFolderShortcutGuideAssetsInstallFolder" Directory="ShortcutGuideAssetsFolder" On="uninstall"/>
</Component>
<Component Id="RemoveShortcutGuideManifestsFolder" Guid="F47E2C3A-8D91-4B6F-A2E5-9C8D7F6A1B3E" Directory="ShortcutGuideManifestsFolder" >
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemoveShortcutGuideManifestsFolder" Value="" KeyPath="yes"/>
</RegistryKey>
<RemoveFolder Id="RemoveFolderShortcutGuideManifestsInstallFolder" Directory="ShortcutGuideManifestsFolder" On="uninstall"/>
</Component>
</ComponentGroup>
</Fragment>

View File

@@ -28,7 +28,7 @@ Function Generate-FileList() {
$fileExclusionList = @("*.pdb", "*.lastcodeanalysissucceeded", "createdump.exe", "powertoys.exe")
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri")
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri", "*.yml")
# MFC DLLs leak into the output via WindowsAppSDKSelfContained but no PowerToys binary imports them.
# Verified with dumpbin /dependents across all 2176 binaries — zero consumers.
@@ -112,6 +112,8 @@ Function Generate-FileComponents() {
foreach ($file in $fileList) {
$fileTmp = $file -replace "-", "_"
$fileTmp = $fileTmp -replace "[^A-Za-z0-9_.]", "_"
if ($fileTmp -match "^[^A-Za-z_]") { $fileTmp = "_$fileTmp" }
$componentDefs +=
@"
<File Id="$($fileListName)_File_$($fileTmp)" Source="`$(var.$($fileListName)Path)\$($file)" />`r`n
@@ -397,8 +399,24 @@ Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePat
## Plugins
#ShortcutGuide
# Ensure manifest yml files are in the build output (the Build target's CopyToOutputDirectory
# may not run reliably under -graph mode in solution builds).
$sgManifestsSrc = "$PSScriptRoot..\..\..\src\modules\ShortcutGuide\ShortcutGuide.Ui\Assets\ShortcutGuide\Manifests"
$sgManifestsDst = "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ShortcutGuide\Manifests"
Write-Host "ShortcutGuide manifests: src=$sgManifestsSrc exists=$(Test-Path $sgManifestsSrc)"
Write-Host "ShortcutGuide manifests: dst=$sgManifestsDst exists=$(Test-Path $sgManifestsDst)"
if (Test-Path $sgManifestsSrc) {
New-Item -Path $sgManifestsDst -ItemType Directory -Force | Out-Null
Copy-Item "$sgManifestsSrc\*.yml" -Destination $sgManifestsDst -Force
$copied = (Get-ChildItem "$sgManifestsDst\*.yml" -ErrorAction SilentlyContinue).Count
Write-Host "ShortcutGuide manifests: copied $copied yml files to build output"
} else {
Write-Host "WARNING: ShortcutGuide manifest source not found at $sgManifestsSrc"
}
Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideAssetsFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ShortcutGuide\"
Generate-FileComponents -fileListName "ShortcutGuideAssetsFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -regroot $registryroot
Generate-FileComponents -fileListName "ShortcutGuideAssetsFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs
Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideManifestsFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ShortcutGuide\Manifests\"
Generate-FileComponents -fileListName "ShortcutGuideManifestsFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs
#Settings
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\"

View File

@@ -48,8 +48,6 @@ private:
void ClearDrawingPoint();
void ClearDrawing();
void BringToFront();
// Spawn a ClickLight-style expanding ripple pulse for the given click.
void SpawnRipplePulse(MouseButton button);
HHOOK m_mouseHook = NULL;
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept;
// Helpers for spotlight overlay
@@ -86,7 +84,6 @@ private:
bool m_rightPointerEnabled = true;
bool m_alwaysPointerEnabled = true;
bool m_spotlightMode = false;
bool m_rippleMode = false;
bool m_leftButtonPressed = false;
bool m_rightButtonPressed = false;
@@ -332,18 +329,6 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
switch (wParam)
{
case WM_LBUTTONDOWN:
if (instance->m_rippleMode)
{
if (instance->m_leftPointerEnabled)
{
instance->SpawnRipplePulse(MouseButton::Left);
if (instance->m_timer_id == 0)
{
instance->m_timer_id = SetTimer(instance->m_hwnd, BRING_TO_FRONT_TIMER_ID, 10, nullptr);
}
}
break;
}
if (instance->m_leftPointerEnabled)
{
if (instance->m_alwaysPointerEnabled && !instance->m_rightButtonPressed)
@@ -369,18 +354,6 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
}
break;
case WM_RBUTTONDOWN:
if (instance->m_rippleMode)
{
if (instance->m_rightPointerEnabled)
{
instance->SpawnRipplePulse(MouseButton::Right);
if (instance->m_timer_id == 0)
{
instance->m_timer_id = SetTimer(instance->m_hwnd, BRING_TO_FRONT_TIMER_ID, 10, nullptr);
}
}
break;
}
if (instance->m_rightPointerEnabled)
{
if (instance->m_alwaysPointerEnabled && !instance->m_leftButtonPressed)
@@ -505,7 +478,6 @@ void Highlighter::ApplySettings(MouseHighlighterSettings settings)
m_rightPointerEnabled = settings.rightButtonColor.A != 0;
m_alwaysPointerEnabled = settings.alwaysColor.A != 0;
m_spotlightMode = settings.spotlightMode && settings.alwaysColor.A != 0;
m_rippleMode = settings.rippleMode && !m_spotlightMode;
if (m_spotlightMode)
{
@@ -671,149 +643,6 @@ void Highlighter::UpdateSpotlightMask(float cx, float cy, float radius, bool sho
}
}
// Spawn a ClickLight-inspired expanding ring + glow pulse at the cursor.
// Each click emits a transient, self-cleaning pulse — pulses can overlap.
void Highlighter::SpawnRipplePulse(MouseButton button)
{
if (!m_compositor || !m_shape)
{
return;
}
POINT pt;
GetCursorPos(&pt);
ScreenToClient(m_hwnd, &pt);
winrt::Windows::UI::Color color;
if (button == MouseButton::Left)
{
color = m_leftClickColor;
}
else if (button == MouseButton::Right)
{
color = m_rightClickColor;
}
else
{
color = m_alwaysColor;
}
if (color.A == 0)
{
return;
}
const float baseRadius = (m_radius < 1.0f) ? 1.0f : m_radius;
// ClickLight-style proportions: ring expands from ~0.20× to ~1.05× of the base radius,
// glow halo follows from ~0.30× to ~1.40× at a lower intensity.
const float ringStart = baseRadius * 0.20f;
const float ringEnd = baseRadius * 1.05f;
const float glowStart = baseRadius * 0.30f;
const float glowEnd = baseRadius * 1.40f;
int duration = m_fadeDuration_ms;
if (duration < 60)
{
duration = 60;
}
if (duration > 2000)
{
duration = 2000;
}
auto dur = std::chrono::milliseconds(duration);
// Cubic ease-out (matches ClickLight's 1 - (1 - p)^3 feel).
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
// Final, fully-transparent target color (preserves RGB to avoid mid-animation tint shifts).
auto transparentColor = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
// Glow (filled, low-alpha halo).
auto glowColor = winrt::Windows::UI::ColorHelper::FromArgb(
static_cast<uint8_t>(static_cast<float>(color.A) * 0.35f),
color.R,
color.G,
color.B);
auto glowGeom = m_compositor.CreateEllipseGeometry();
glowGeom.Radius({ glowStart, glowStart });
auto glowBrush = m_compositor.CreateColorBrush(glowColor);
auto glowShape = m_compositor.CreateSpriteShape(glowGeom);
glowShape.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
glowShape.FillBrush(glowBrush);
// Ring (stroked, full color).
auto ringGeom = m_compositor.CreateEllipseGeometry();
ringGeom.Radius({ ringStart, ringStart });
auto ringBrush = m_compositor.CreateColorBrush(color);
auto ringShape = m_compositor.CreateSpriteShape(ringGeom);
ringShape.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
ringShape.StrokeBrush(ringBrush);
ringShape.StrokeThickness((std::max)(2.0f, baseRadius * 0.18f));
ringShape.IsStrokeNonScaling(true);
m_shape.Shapes().Append(glowShape);
m_shape.Shapes().Append(ringShape);
// Radius animations (scale-equivalent, keeps stroke crisp).
auto glowSizeAnim = m_compositor.CreateVector2KeyFrameAnimation();
glowSizeAnim.InsertKeyFrame(0.0f, { glowStart, glowStart });
glowSizeAnim.InsertKeyFrame(1.0f, { glowEnd, glowEnd }, ease);
glowSizeAnim.Duration(dur);
auto ringSizeAnim = m_compositor.CreateVector2KeyFrameAnimation();
ringSizeAnim.InsertKeyFrame(0.0f, { ringStart, ringStart });
ringSizeAnim.InsertKeyFrame(1.0f, { ringEnd, ringEnd }, ease);
ringSizeAnim.Duration(dur);
// Color (alpha) fade-out animations.
auto glowColorAnim = m_compositor.CreateColorKeyFrameAnimation();
glowColorAnim.InsertKeyFrame(0.0f, glowColor);
glowColorAnim.InsertKeyFrame(1.0f, transparentColor, ease);
glowColorAnim.Duration(dur);
auto ringColorAnim = m_compositor.CreateColorKeyFrameAnimation();
ringColorAnim.InsertKeyFrame(0.0f, color);
ringColorAnim.InsertKeyFrame(1.0f, transparentColor, ease);
ringColorAnim.Duration(dur);
// Batch the four animations so we can clean up the shapes when the pulse ends.
auto batch = m_compositor.CreateScopedBatch(winrt::CompositionBatchTypes::Animation);
glowGeom.StartAnimation(L"Radius", glowSizeAnim);
ringGeom.StartAnimation(L"Radius", ringSizeAnim);
glowBrush.StartAnimation(L"Color", glowColorAnim);
ringBrush.StartAnimation(L"Color", ringColorAnim);
batch.End();
auto dispatcher = m_dispatcherQueueController.DispatcherQueue();
batch.Completed([dispatcher, glowShape, ringShape](auto&&, auto&&) {
// Marshal shape removal back to the compositor thread.
dispatcher.TryEnqueue([glowShape, ringShape]() {
try
{
if (Highlighter::instance == nullptr || Highlighter::instance->m_shape == nullptr)
{
return;
}
auto shapes = Highlighter::instance->m_shape.Shapes();
uint32_t index = 0;
if (shapes.IndexOf(ringShape, index))
{
shapes.RemoveAt(index);
}
if (shapes.IndexOf(glowShape, index))
{
shapes.RemoveAt(index);
}
}
catch (...)
{
// Highlighter may have torn down between batch completion and dispatch — ignore.
}
});
});
}
#pragma region MouseHighlighter_API
void MouseHighlighterApplySettings(MouseHighlighterSettings settings)

View File

@@ -19,7 +19,6 @@ struct MouseHighlighterSettings
int fadeDurationMs = MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS;
bool autoActivate = MOUSE_HIGHLIGHTER_DEFAULT_AUTO_ACTIVATE;
bool spotlightMode = false;
bool rippleMode = false;
};
int MouseHighlighterMain(HINSTANCE hinst, MouseHighlighterSettings settings);

View File

@@ -21,7 +21,6 @@ namespace
const wchar_t JSON_KEY_HIGHLIGHT_FADE_DURATION_MS[] = L"highlight_fade_duration_ms";
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t JSON_KEY_SPOTLIGHT_MODE[] = L"spotlight_mode";
const wchar_t JSON_KEY_RIPPLE_MODE[] = L"ripple_mode";
}
extern "C" IMAGE_DOS_HEADER __ImageBase;
@@ -393,16 +392,6 @@ public:
{
Logger::warn("Failed to initialize spotlight mode settings. Will use default value");
}
try
{
// Parse ripple mode
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_RIPPLE_MODE);
highlightSettings.rippleMode = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to initialize ripple mode settings. Will use default value");
}
}
else
{

View File

@@ -8,6 +8,7 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using static ShortcutGuide.Helpers.ResourceLoaderInstance;
@@ -25,6 +26,12 @@ namespace ShortcutGuide.Helpers
{
string path = Path.Combine(ManifestInterpreter.PathOfManifestFiles, $"Microsoft.PowerToys.{ManifestInterpreter.Language}.yml");
if (!File.Exists(path))
{
Logger.LogWarning($"PowerToys manifest file not found: '{path}'. PowerToys-specific shortcuts will not appear in ShortcutGuide.");
return;
}
StringBuilder content = new(File.ReadAllText(path));
const string populateStartString = "# <Populate start>";

View File

@@ -99,7 +99,14 @@ namespace ShortcutGuide
return;
}
PowerToysShortcutsPopulator.Populate();
try
{
PowerToysShortcutsPopulator.Populate();
}
catch (Exception ex)
{
Logger.LogError("Failed to populate PowerToys shortcuts in manifest.", ex);
}
});
CopyAndIndexGenerationThread.IsBackground = true;
CopyAndIndexGenerationThread.Start();

View File

@@ -26,7 +26,6 @@
<Content Remove="Assets\CopilotKey.png" />
<Content Remove="Assets\ShortcutGuide\HeroImage.png" />
<Content Remove="Assets\ShortcutGuide\ShortcutGuide.ico" />
<Content Remove="Assets\ShortcutGuide\Manifests\*.yml" />
</ItemGroup>
<ItemGroup>

View File

@@ -12,8 +12,6 @@
"src\\common\\interop\\PowerToys.Interop.vcxproj",
"src\\common\\logger\\logger.vcxproj",
"src\\common\\version\\version.vcxproj",
"src\\modules\\MouseUtils\\MouseJump.Common\\MouseJump.Common.csproj",
"src\\modules\\ZoomIt\\ZoomItSettingsInterop\\ZoomItSettingsInterop.vcxproj",
"src\\modules\\cmdpal\\CmdPalKeyboardService\\CmdPalKeyboardService.vcxproj",
"src\\modules\\cmdpal\\CmdPalModuleInterface\\CmdPalModuleInterface.vcxproj",
"src\\modules\\cmdpal\\Microsoft.CmdPal.Common\\Microsoft.CmdPal.Common.csproj",
@@ -59,8 +57,7 @@
"src\\modules\\cmdpal\\ext\\SamplePagesExtension\\SamplePagesExtension.csproj",
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions.Toolkit\\Microsoft.CommandPalette.Extensions.Toolkit.csproj",
"src\\modules\\cmdpal\\extensionsdk\\Microsoft.CommandPalette.Extensions\\Microsoft.CommandPalette.Extensions.vcxproj",
"src\\modules\\powerdisplay\\PowerDisplay.Models\\PowerDisplay.Models.csproj",
"src\\settings-ui\\Settings.UI.Library\\Settings.UI.Library.csproj"
]
}
}
}

View File

@@ -76,8 +76,13 @@ internal sealed partial class HttpResourceCacheHandler : DelegatingHandler
lock (_lock)
{
foreach (var inflightKey in _inflightFetches.Keys)
foreach (var (inflightKey, task) in _inflightFetches)
{
if (task.IsCompleted)
{
continue;
}
if (!Uri.TryCreate(inflightKey, UriKind.Absolute, out var inflightUri))
{
continue;

View File

@@ -208,7 +208,10 @@ public partial class ListItemViewModel : CommandItemViewModel
contextItemViewModel.Command.Id == ShowDetailsCommand.ShowDetailsCommandId))
{
var showDetailsCommand = new ShowDetailsCommand(Details);
var showDetailsContextItem = new CommandContextItem(showDetailsCommand);
var showDetailsContextItem = new CommandContextItem(showDetailsCommand)
{
Icon = showDetailsCommand.Icon,
};
var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext);
showDetailsContextItemViewModel.SlowInitializeProperties();
UnsafeMoreCommands.Add(showDetailsContextItemViewModel);
@@ -249,7 +252,10 @@ public partial class ListItemViewModel : CommandItemViewModel
}
var showDetailsCommand = new ShowDetailsCommand(Details);
var showDetailsContextItem = new CommandContextItem(showDetailsCommand);
var showDetailsContextItem = new CommandContextItem(showDetailsCommand)
{
Icon = showDetailsCommand.Icon,
};
var showDetailsContextItemViewModel = new CommandContextItemViewModel(showDetailsContextItem, PageContext);
showDetailsContextItemViewModel.SlowInitializeProperties();
UnsafeMoreCommands.Add(showDetailsContextItemViewModel);

View File

@@ -1212,6 +1212,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Hide details.
/// </summary>
public static string HideDetailsCommand {
get {
return ResourceManager.GetString("HideDetailsCommand", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} of {1}.
/// </summary>

View File

@@ -307,6 +307,10 @@
<value>Show details</value>
<comment>Name for the command that shows details of an item</comment>
</data>
<data name="HideDetailsCommand" xml:space="preserve">
<value>Hide details</value>
<comment>Name for the command that hides details of an item</comment>
</data>
<data name="PinnedItemSuffix" xml:space="preserve">
<value>Pinned</value>
<comment>Suffix shown for pinned items in the dock</comment>

View File

@@ -500,6 +500,18 @@ public partial class ShellViewModel : ObservableObject,
WeakReferenceMessenger.Default.Send<GoHomeMessage>(new(withAnimation, focusSearch));
}
/// <summary>
/// Resets navigation to the root page, clearing any transient state.
/// Use when entering from a hotkey while the palette may already be
/// showing a transient dock page.
/// </summary>
public void ResetToHome()
{
_currentlyTransient = false;
_rootPageService.GoHome();
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(new ExtensionObject<ICommand>(_rootPage)));
}
public void GoBack(bool withAnimation = true, bool focusSearch = true)
{
WeakReferenceMessenger.Default.Send<GoBackMessage>(new(withAnimation, focusSearch));

View File

@@ -12,22 +12,39 @@ public sealed partial class ShowDetailsCommand : InvokableCommand
{
public static string ShowDetailsCommandId { get; } = "com.microsoft.cmdpal.showDetails";
private static IconInfo IconInfo { get; } = new IconInfo("\uF000"); // KnowledgeArticle Icon
private static IconInfo ShowIcon { get; } = new IconInfo("\uF000"); // KnowledgeArticle Icon
private static IconInfo HideIcon { get; } = new IconInfo("\uED1A"); // Hide Icon
private DetailsViewModel Details { get; set; }
private bool _isDetailsVisible;
public ShowDetailsCommand(DetailsViewModel details)
{
Id = ShowDetailsCommandId;
Name = UI.ViewModels.Properties.Resources.ShowDetailsCommand;
Icon = IconInfo;
Icon = ShowIcon;
Details = details;
}
public override CommandResult Invoke()
{
// Send the ShowDetailsMessage when the action is invoked
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(Details));
_isDetailsVisible = !_isDetailsVisible;
if (_isDetailsVisible)
{
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(Details));
Name = UI.ViewModels.Properties.Resources.HideDetailsCommand;
Icon = HideIcon;
}
else
{
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
Name = UI.ViewModels.Properties.Resources.ShowDetailsCommand;
Icon = ShowIcon;
}
return CommandResult.KeepOpen();
}
}

View File

@@ -6,6 +6,7 @@ using AdaptiveCards.ObjectModel.WinUI3;
using AdaptiveCards.Rendering.WinUI3;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
@@ -101,11 +102,26 @@ public sealed partial class ContentFormControl : UserControl
// Use the Loaded event to ensure we focus after the card is in the visual tree
_renderedCard.FrameworkElement.Loaded += OnFrameworkElementLoaded;
// Use LayoutUpdated to fix accessibility after the full visual tree is materialized.
// Loaded fires too early — the Adaptive Card renderer may not have finished building
// the control tree by then.
_renderedCard.FrameworkElement.LayoutUpdated += OnFrameworkElementLayoutUpdated;
}
_renderedCard.Action += Rendered_Action;
}
private void OnFrameworkElementLayoutUpdated(object? sender, object e)
{
// Only fix once — unhook after first layout pass
if (_renderedCard?.FrameworkElement is FrameworkElement element)
{
element.LayoutUpdated -= OnFrameworkElementLayoutUpdated;
FixToggleAccessibilityNames(element);
}
}
private void OnFrameworkElementLoaded(object sender, RoutedEventArgs e)
{
// Unhook the event handler to avoid multiple registrations
@@ -127,6 +143,109 @@ public sealed partial class ContentFormControl : UserControl
}
}
/// <summary>
/// Fixes missing AutomationProperties.Name on CheckBox and ToggleSwitch controls
/// rendered by the Adaptive Cards library (AdaptiveCards.Rendering.WinUI3 v2.x).
/// Without this fix, Narrator announces "space, checkbox, checked" instead of the
/// actual setting label. This method walks the tree, finds CheckBox/ToggleSwitch
/// controls missing an automation name, and sets it from the adjacent label TextBlock.
/// </summary>
private static void FixToggleAccessibilityNames(DependencyObject root)
{
var childCount = VisualTreeHelper.GetChildrenCount(root);
for (var i = 0; i < childCount; i++)
{
var child = VisualTreeHelper.GetChild(root, i);
if (child is CheckBox checkBox)
{
var existingName = AutomationProperties.GetName(checkBox);
if (string.IsNullOrEmpty(existingName))
{
// In Adaptive Cards, the label TextBlock is a sibling to the CheckBox's
// container within a shared Grid parent. Walk up to find that Grid, then
// search for a TextBlock with actual text content.
var labelText = FindAdjacentLabel(checkBox);
if (!string.IsNullOrEmpty(labelText))
{
AutomationProperties.SetName(checkBox, labelText);
}
}
}
else if (child is ToggleSwitch toggleSwitch)
{
var existingName = AutomationProperties.GetName(toggleSwitch);
if (string.IsNullOrEmpty(existingName))
{
var labelText = FindAdjacentLabel(toggleSwitch);
if (!string.IsNullOrEmpty(labelText))
{
AutomationProperties.SetName(toggleSwitch, labelText);
}
}
}
// Recurse into children
FixToggleAccessibilityNames(child);
}
}
/// <summary>
/// Walks up the visual tree from a control to find the nearest parent Grid,
/// then searches that Grid's descendants for a TextBlock with non-whitespace text.
/// This handles the Adaptive Cards layout where the label TextBlock is a sibling
/// of the CheckBox's container within a shared Grid row.
/// </summary>
private static string? FindAdjacentLabel(FrameworkElement control)
{
// Walk up the tree to find the nearest Grid ancestor (the row container)
DependencyObject? current = control;
Grid? parentGrid = null;
for (var depth = 0; depth < 10 && current != null; depth++)
{
current = VisualTreeHelper.GetParent(current);
if (current is Grid grid)
{
// Find the grid that contains multiple children (the row container)
if (VisualTreeHelper.GetChildrenCount(grid) > 1)
{
parentGrid = grid;
break;
}
}
}
if (parentGrid == null)
{
return null;
}
// Search the parent Grid for a TextBlock with actual text
return FindFirstNonEmptyTextBlock(parentGrid);
}
private static string? FindFirstNonEmptyTextBlock(DependencyObject root)
{
var childCount = VisualTreeHelper.GetChildrenCount(root);
for (var i = 0; i < childCount; i++)
{
var child = VisualTreeHelper.GetChild(root, i);
if (child is TextBlock tb && !string.IsNullOrWhiteSpace(tb.Text))
{
return tb.Text;
}
var result = FindFirstNonEmptyTextBlock(child);
if (result != null)
{
return result;
}
}
return null;
}
private Control? FindFirstFocusableElement(DependencyObject parent)
{
var childCount = VisualTreeHelper.GetChildrenCount(parent);

View File

@@ -120,6 +120,7 @@ public sealed partial class DockItemControl : Control
{
control.UpdateTextVisibility();
control.UpdateAlignment();
control.UpdateCompactState();
}
}

View File

@@ -129,6 +129,9 @@ public sealed partial class DockWindow : WindowEx,
overlappedPresenter.IsResizable = false;
}
_hwnd = GetWindowHandle(this);
_dock.OwnerHwnd = (nint)_hwnd;
// immediately when we're created: make sure to remove our window frame
// and shadow. We don't _always_ get an Activated when we're first
// created.
@@ -139,9 +142,6 @@ public sealed partial class DockWindow : WindowEx,
WeakReferenceMessenger.Default.Register<RequestShowPaletteAtMessage>(this);
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
_hwnd = GetWindowHandle(this);
_dock.OwnerHwnd = (nint)_hwnd;
// Subclass the window to intercept messages
//
// Set up custom window procedure to listen for display changes

View File

@@ -39,6 +39,58 @@
</UserControl.Resources>
<ScrollViewer>
<StackPanel Width="420" Spacing="16">
<!-- Monitor Selector (hidden when only one monitor) -->
<StackPanel
x:Name="MonitorSelectorPanel"
Spacing="8"
Visibility="Collapsed">
<TextBlock
x:Uid="PinToDock_MonitorHeader"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<ComboBox x:Name="MonitorComboBox" HorizontalAlignment="Stretch" />
</StackPanel>
<!-- Dock Section Selector -->
<StackPanel Spacing="8">
<TextBlock
x:Uid="PinToDock_SectionHeader"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<tkcontrols:Segmented
x:Name="SectionSegmented"
HorizontalAlignment="Stretch"
SelectedIndex="0"
SelectionMode="Single">
<tkcontrols:SegmentedItem x:Name="StartSegmentedItem" x:Uid="PinToDock_Start" />
<tkcontrols:SegmentedItem x:Name="CenterSegmentedItem" x:Uid="PinToDock_Center" />
<tkcontrols:SegmentedItem x:Name="EndSegmentedItem" x:Uid="PinToDock_End" />
</tkcontrols:Segmented>
</StackPanel>
<!-- Label Options -->
<StackPanel Spacing="4">
<CheckBox
x:Name="ShowTitleCheckBox"
x:Uid="PinToDock_ShowTitle"
Checked="OnLabelOptionChanged"
IsChecked="True"
Unchecked="OnLabelOptionChanged" />
<CheckBox
x:Name="ShowSubtitleCheckBox"
x:Uid="PinToDock_ShowSubtitle"
Checked="OnLabelOptionChanged"
IsChecked="True"
Unchecked="OnLabelOptionChanged" />
</StackPanel>
<Rectangle
Height="1"
Margin="-24,0,-24,0"
HorizontalAlignment="Stretch"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
<!-- Preview -->
<StackPanel Spacing="8">
<Border
@@ -83,57 +135,6 @@
</Grid>
</Border>
</StackPanel>
<Rectangle
Height="1"
Margin="-24,0,-24,0"
HorizontalAlignment="Stretch"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
<!-- Dock Section Selector -->
<StackPanel Spacing="8">
<TextBlock
x:Uid="PinToDock_SectionHeader"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<tkcontrols:Segmented
x:Name="SectionSegmented"
HorizontalAlignment="Stretch"
SelectedIndex="0"
SelectionMode="Single">
<tkcontrols:SegmentedItem x:Name="StartSegmentedItem" x:Uid="PinToDock_Start" />
<tkcontrols:SegmentedItem x:Name="CenterSegmentedItem" x:Uid="PinToDock_Center" />
<tkcontrols:SegmentedItem x:Name="EndSegmentedItem" x:Uid="PinToDock_End" />
</tkcontrols:Segmented>
</StackPanel>
<!-- Monitor Selector (hidden when only one monitor) -->
<StackPanel
x:Name="MonitorSelectorPanel"
Spacing="8"
Visibility="Collapsed">
<TextBlock
x:Uid="PinToDock_MonitorHeader"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<ComboBox x:Name="MonitorComboBox" HorizontalAlignment="Stretch" />
</StackPanel>
<!-- Label Options -->
<StackPanel Spacing="4">
<CheckBox
x:Name="ShowTitleCheckBox"
x:Uid="PinToDock_ShowTitle"
Checked="OnLabelOptionChanged"
IsChecked="True"
Unchecked="OnLabelOptionChanged" />
<CheckBox
x:Name="ShowSubtitleCheckBox"
x:Uid="PinToDock_ShowSubtitle"
Checked="OnLabelOptionChanged"
IsChecked="True"
Unchecked="OnLabelOptionChanged" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -448,8 +448,9 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
if (isPage)
{
// If we're here, then the bound command was a page
// of some kind. Let's pop the stack, show the window, and navigate to it.
GoHome(false);
// of some kind. Reset to root (clearing any transient dock state),
// show the window, and navigate to it.
ViewModel.ResetToHome();
WeakReferenceMessenger.Default.Send<ShowWindowMessage>(new(message.Hwnd));
}

View File

@@ -219,6 +219,7 @@
AutomationProperties.AutomationId="{x:Bind Id, Mode=OneWay}"
Click="SettingsCard_Click"
DataContext="{x:Bind}"
DataContextChanged="SettingsCard_DataContextChanged"
Description="{x:Bind ExtensionSubtext, Mode=OneWay}"
Header="{x:Bind DisplayName, Mode=OneWay}"
IsClickEnabled="True">

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.Controls;
using ManagedCommon;
@@ -9,6 +10,7 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
@@ -19,6 +21,7 @@ public sealed partial class ExtensionsPage : Page
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
private readonly SettingsViewModel? viewModel;
private readonly Dictionary<string, WeakReference<SettingsCard>> _vmToCardMap = new();
public ExtensionsPage()
{
@@ -41,6 +44,38 @@ public sealed partial class ExtensionsPage : Page
}
}
private void SettingsCard_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
// Store the card reference keyed by Id (not the VM itself) to avoid leaking VM references
if (sender is SettingsCard card && card.DataContext is ProviderSettingsViewModel newVm)
{
_vmToCardMap[newVm.Id] = new WeakReference<SettingsCard>(card);
newVm.PropertyChanged += ProviderViewModel_PropertyChanged;
// Immediately update automation name in case DisplayName is already available
if (card.Content is ToggleSwitch toggle && !string.IsNullOrEmpty(newVm.DisplayName))
{
AutomationProperties.SetName(toggle, newVm.DisplayName);
}
}
}
private void ProviderViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
// When DisplayName changes, update the ToggleSwitch's automation name
if (e.PropertyName == nameof(ProviderSettingsViewModel.DisplayName) && sender is ProviderSettingsViewModel vm && !string.IsNullOrEmpty(vm.DisplayName))
{
// Get the card reference from our map
if (_vmToCardMap.TryGetValue(vm.Id, out var cardRef) && cardRef.TryGetTarget(out var card))
{
if (card.Content is ToggleSwitch toggle)
{
AutomationProperties.SetName(toggle, vm.DisplayName);
}
}
}
}
private void OnFindInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
SearchBox?.Focus(FocusState.Keyboard);

View File

@@ -1137,11 +1137,11 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<comment>Header for the preview section in the pin to dock dialog</comment>
</data>
<data name="PinToDock_SectionHeader.Text" xml:space="preserve">
<value>Choose where to pin this command</value>
<value>Position</value>
<comment>Header for the dock section selector in the pin to dock dialog</comment>
</data>
<data name="PinToDock_MonitorHeader.Text" xml:space="preserve">
<value>Choose which monitor</value>
<value>Monitor</value>
<comment>Header for the monitor selector in the pin to dock dialog</comment>
</data>
<data name="PinToDock_Start.Content" xml:space="preserve">

View File

@@ -52,9 +52,9 @@ public class FallbackRemoteDesktopItemTests
fallback.UpdateQuery(hostname);
// Assert
var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostname);
Assert.AreEqual(expectedTitle, fallback.Title);
Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle);
Assert.AreEqual(Resources.remotedesktop_title, fallback.Title);
var expectedSubtitleArbitrary = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostname);
Assert.AreEqual(expectedSubtitleArbitrary, fallback.Subtitle);
var command = fallback.Command as OpenRemoteDesktopCommand;
Assert.IsNotNull(command);
@@ -110,9 +110,9 @@ public class FallbackRemoteDesktopItemTests
fallback.UpdateQuery(hostPort);
// Assert
var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostPort);
Assert.AreEqual(expectedTitle, fallback.Title);
Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle);
Assert.AreEqual(Resources.remotedesktop_title, fallback.Title);
var expectedSubtitleHostPort = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostPort);
Assert.AreEqual(expectedSubtitleHostPort, fallback.Subtitle);
var command = fallback.Command as OpenRemoteDesktopCommand;
Assert.IsNotNull(command);
@@ -131,9 +131,9 @@ public class FallbackRemoteDesktopItemTests
fallback.UpdateQuery(hostPort);
// Assert
var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostPort);
Assert.AreEqual(expectedTitle, fallback.Title);
Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle);
Assert.AreEqual(Resources.remotedesktop_title, fallback.Title);
var expectedSubtitleHostPort = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostPort);
Assert.AreEqual(expectedSubtitleHostPort, fallback.Subtitle);
var command = fallback.Command as OpenRemoteDesktopCommand;
Assert.IsNotNull(command);

View File

@@ -0,0 +1,189 @@
// 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.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
[TestClass]
public class RemoteDesktopListPageTests
{
// Helper: create a RemoteDesktopListPage backed by mock connections.
private static RemoteDesktopListPage CreatePage(params string[] connectionNames)
{
var settingsManager = new MockSettingsManager(connectionNames);
var connectionsManager = new MockRdpConnectionsManager(settingsManager);
return new RemoteDesktopListPage(connectionsManager);
}
[TestMethod]
public void EmptyQuery_ReturnsOnlyExistingConnections()
{
// Arrange
var page = CreatePage("server1", "server2");
// Act
page.UpdateSearchText(string.Empty, string.Empty);
var items = page.GetItems();
// Assert — only the 2 preexisting connections, no arbitrary host item
Assert.AreEqual(2, items.Length);
}
[TestMethod]
public void WhitespaceQuery_ReturnsOnlyExistingConnections()
{
// Arrange
var page = CreatePage("server1");
// Act
page.UpdateSearchText(string.Empty, " ");
var items = page.GetItems();
// Assert
Assert.AreEqual(1, items.Length);
}
[TestMethod]
public void ValidHostname_NotMatchingConnection_PrependsArbitraryItem()
{
// Arrange
var page = CreatePage("server1");
// Act
page.UpdateSearchText(string.Empty, "test.corp");
var items = page.GetItems();
// Assert — arbitrary item prepended, then the existing connection
Assert.AreEqual(2, items.Length);
var firstItem = items[0] as ConnectionListItem;
Assert.IsNotNull(firstItem, "First item should be a ConnectionListItem for the arbitrary host");
Assert.AreEqual("test.corp", firstItem.ConnectionName);
}
[TestMethod]
public void QueryExactlyMatchingExistingConnection_NoArbitraryItem()
{
// Arrange
var page = CreatePage("rdp-server");
// Act
page.UpdateSearchText(string.Empty, "rdp-server");
var items = page.GetItems();
// Assert — no extra item; count stays at 1
Assert.AreEqual(1, items.Length);
var item = items[0] as ConnectionListItem;
Assert.IsNotNull(item);
Assert.AreEqual("rdp-server", item.ConnectionName);
}
[TestMethod]
public void InvalidHostname_ReturnsOnlyExistingConnections()
{
// Arrange
var page = CreatePage("server1");
// Act
page.UpdateSearchText(string.Empty, "!!!invalid");
var items = page.GetItems();
// Assert — no arbitrary item for an invalid hostname
Assert.AreEqual(1, items.Length);
}
[TestMethod]
public void ValidHostnameWithPort_ArbitraryItemIncludesPort()
{
// Arrange
var page = CreatePage("server1");
// Act
page.UpdateSearchText(string.Empty, "localhost:3389");
var items = page.GetItems();
// Assert — full host:port string preserved as the ConnectionName
Assert.AreEqual(2, items.Length);
var firstItem = items[0] as ConnectionListItem;
Assert.IsNotNull(firstItem);
Assert.AreEqual("localhost:3389", firstItem.ConnectionName);
}
[TestMethod]
public void SequentialCalls_ValidThenEmpty_ClearsArbitraryItem()
{
// Arrange
var page = CreatePage("server1");
// Act — first call adds arbitrary item
page.UpdateSearchText(string.Empty, "test.corp");
var itemsAfterValid = page.GetItems();
// Assert — arbitrary item present
Assert.AreEqual(2, itemsAfterValid.Length);
var firstItem = itemsAfterValid[0] as ConnectionListItem;
Assert.IsNotNull(firstItem);
Assert.AreEqual("test.corp", firstItem.ConnectionName);
// Act — second call clears it
page.UpdateSearchText("test.corp", string.Empty);
var itemsAfterEmpty = page.GetItems();
// Assert — back to only existing connections
Assert.AreEqual(1, itemsAfterEmpty.Length);
}
[TestMethod]
public void ValidHostname_NoExistingConnections_ReturnsSingleArbitraryItem()
{
// Arrange — no preexisting connections
var page = CreatePage();
// Act
page.UpdateSearchText(string.Empty, "alpha.corp");
var items = page.GetItems();
// Assert
Assert.AreEqual(1, items.Length);
var firstItem = items[0] as ConnectionListItem;
Assert.IsNotNull(firstItem);
Assert.AreEqual("alpha.corp", firstItem.ConnectionName);
}
[TestMethod]
public void IPv4Address_ReturnsArbitraryItem()
{
// Arrange
var page = CreatePage();
// Act
page.UpdateSearchText(string.Empty, "192.168.1.100");
var items = page.GetItems();
// Assert
Assert.AreEqual(1, items.Length);
var firstItem = items[0] as ConnectionListItem;
Assert.IsNotNull(firstItem);
Assert.AreEqual("192.168.1.100", firstItem.ConnectionName);
}
[TestMethod]
public void IPv4AddressWithPort_ReturnsArbitraryItemWithPort()
{
// Arrange
var page = CreatePage();
// Act
page.UpdateSearchText(string.Empty, "192.168.1.100:3390");
var items = page.GetItems();
// Assert — full IP:port string preserved
Assert.AreEqual(1, items.Length);
var firstItem = items[0] as ConnectionListItem;
Assert.IsNotNull(firstItem);
Assert.AreEqual("192.168.1.100:3390", firstItem.ConnectionName);
}
}

View File

@@ -30,8 +30,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, IDispo
private const uint HardQueryCookie = 10;
private static readonly NoOpCommand BaseCommandWithId = new() { Id = CommandId };
private readonly CompositeFormat _fallbackItemSearchPageTitleFormat = CompositeFormat.Parse(Resources.Indexer_fallback_searchPage_title);
private readonly CompositeFormat _fallbackItemSearchSubtitleMultipleResults = CompositeFormat.Parse(Resources.Indexer_Fallback_MultipleResults_Subtitle);
private readonly CompositeFormat _fallbackItemSearchSubtitleFormat = CompositeFormat.Parse(Resources.Indexer_fallback_searchPage_title);
private readonly Lock _querySwitchLock = new();
private readonly Lock _resultLock = new();
@@ -152,8 +151,8 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, IDispo
var indexerPage = new IndexerPage(query);
var set = UpdateResultForCurrentQuery(
string.Format(CultureInfo.CurrentCulture, _fallbackItemSearchPageTitleFormat, query),
string.Format(CultureInfo.CurrentCulture, _fallbackItemSearchSubtitleMultipleResults),
Resources.IndexerCommandsProvider_DisplayName,
string.Format(CultureInfo.CurrentCulture, _fallbackItemSearchSubtitleFormat, query),
Icons.FileExplorerIcon,
indexerPage,
MoreCommands,

View File

@@ -193,7 +193,7 @@
<value>Search files</value>
</data>
<data name="Indexer_fallback_searchPage_title" xml:space="preserve">
<value>Search for "{0}" in files</value>
<value>Search for {0} in files</value>
</data>
<data name="Indexer_NoResultsMessage" xml:space="preserve">
<value>No items found</value>

View File

@@ -212,6 +212,11 @@ internal sealed partial class GPUStats : PerformanceCounterSourceBase, IDisposab
internal string CreateGPUImageUrl(int gpuChartIndex)
{
if (_stats.Count <= gpuChartIndex)
{
return string.Empty;
}
return ChartHelper.CreateImageUrl(_stats[gpuChartIndex].GpuChartValues, ChartHelper.ChartType.GPU);
}

View File

@@ -270,8 +270,8 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
};
return _batteryItem is not null
? new[] { _cpuItem!, _memoryItem!, _networkDownItem!, _networkUpItem!, _gpuItem!, _batteryItem! }
: new[] { _cpuItem!, _memoryItem!, _networkDownItem!, _networkUpItem!, _gpuItem! };
? new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem!, _batteryItem! }
: new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem! };
}
}

View File

@@ -34,7 +34,7 @@ internal sealed class SettingsManager : JsonSettingsManager
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{Namespace}.settings.json");
}
public SettingsManager()

View File

@@ -58,7 +58,7 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
if (!File.Exists(settingsPath))
{
return false;
return true;
}
var json = File.ReadAllText(settingsPath);
@@ -72,9 +72,9 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
}
catch
{
// If we can't read the setting, default to not showing the command
// If we can't read the setting, default to showing the command
}
return false;
return true;
}
}

View File

@@ -79,8 +79,8 @@ internal sealed partial class FallbackRemoteDesktopItem : FallbackCommandItem
{
var connectionName = query.Trim();
Command = new OpenRemoteDesktopCommand(connectionName);
Title = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
Subtitle = Resources.remotedesktop_title;
Title = Resources.remotedesktop_title;
Subtitle = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
}
else
{

View File

@@ -1,8 +1,10 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CommandPalette.Extensions;
@@ -10,9 +12,16 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
internal sealed partial class RemoteDesktopListPage : ListPage
internal sealed partial class RemoteDesktopListPage : DynamicListPage
{
private static readonly UriHostNameType[] ValidUriHostNameTypes = [
UriHostNameType.IPv6,
UriHostNameType.IPv4,
UriHostNameType.Dns,
];
private readonly IRdpConnectionsManager _rdpConnectionsManager;
private ConnectionListItem? _arbitraryHostItem;
public RemoteDesktopListPage(IRdpConnectionsManager rdpConnectionsManager)
{
@@ -23,5 +32,68 @@ internal sealed partial class RemoteDesktopListPage : ListPage
_rdpConnectionsManager = rdpConnectionsManager;
}
public override IListItem[] GetItems() => _rdpConnectionsManager.Connections.ToArray();
public override void UpdateSearchText(string oldSearch, string newSearch)
{
var query = newSearch?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(query))
{
_arbitraryHostItem = null;
RaiseItemsChanged(0);
return;
}
var connections = _rdpConnectionsManager.Connections.Where(w => !string.IsNullOrWhiteSpace(w.ConnectionName));
var existingMatch = ConnectionHelpers.FindConnection(query, connections);
if (existingMatch is not null && string.Equals(existingMatch.ConnectionName, query, StringComparison.OrdinalIgnoreCase))
{
// Exact match — no need for an arbitrary host item
_arbitraryHostItem = null;
}
else if (IsValidHostname(query))
{
_arbitraryHostItem = new ConnectionListItem(query);
}
else
{
_arbitraryHostItem = null;
}
RaiseItemsChanged(0);
}
public override IListItem[] GetItems()
{
var connections = _rdpConnectionsManager.Connections.ToArray();
if (_arbitraryHostItem is null)
{
return connections;
}
// Prepend the arbitrary host item so it appears at the top
var result = new IListItem[connections.Length + 1];
result[0] = _arbitraryHostItem;
connections.CopyTo(result, 1);
return result;
}
private static bool IsValidHostname(string query)
{
// Strip port suffix (e.g. "host:3389") before validation,
// since Uri.CheckHostName does not accept host:port strings.
var hostForValidation = query;
var lastColon = hostForValidation.LastIndexOf(':');
if (lastColon > 0 && lastColon < hostForValidation.Length - 1)
{
var portPart = hostForValidation.Substring(lastColon + 1);
if (ushort.TryParse(portPart, out _))
{
hostForValidation = hostForValidation.Substring(0, lastColon);
}
}
return ValidUriHostNameTypes.Contains(Uri.CheckHostName(hostForValidation));
}
}

View File

@@ -45,6 +45,6 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem
var isEmpty = string.IsNullOrEmpty(query);
_executeItem.Name = isEmpty ? string.Empty : Resources.open_in_default_browser;
Title = isEmpty ? string.Empty : UpdateBrowserName(_browserInfoService);
Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
Subtitle = isEmpty ? string.Empty : string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
}
}

View File

@@ -56,10 +56,9 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
_executeItem.Url = query;
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Resources.open_in_default_browser;
Title = string.Format(CultureInfo.CurrentCulture, PluginOpenURL, query);
var browserName = _browserInfoService.GetDefaultBrowser()?.Name;
Subtitle = string.IsNullOrWhiteSpace(browserName) ? Resources.open_in_default_browser : string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, browserName);
Title = string.IsNullOrWhiteSpace(browserName) ? Resources.open_in_default_browser : string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, browserName);
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpenURL, query);
}
private static bool IsValidUrl(string url)

View File

@@ -164,7 +164,7 @@
<value>Search the web in {0}</value>
</data>
<data name="plugin_open_url" xml:space="preserve">
<value>Open "{0}"</value>
<value>Open {0}</value>
</data>
<data name="plugin_open_url_in_browser" xml:space="preserve">
<value>Open url in {0}</value>
@@ -179,7 +179,7 @@
<value>Settings</value>
</data>
<data name="web_search_fallback_subtitle" xml:space="preserve">
<value>Search for "{0}"</value>
<value>Search for {0}</value>
</data>
<data name="open_url_fallback_title" xml:space="preserve">
<value>Open URL</value>

View File

@@ -4,6 +4,7 @@
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.CmdPal.Ext.WindowsSettings.Commands;
using Microsoft.CmdPal.Ext.WindowsSettings.Helpers;
using Microsoft.CmdPal.Ext.WindowsSettings.Properties;
@@ -17,7 +18,7 @@ internal sealed partial class FallbackWindowsSettingsItem : FallbackCommandItem
private readonly Classes.WindowsSettings _windowsSettings;
private readonly string _title = Resources.settings_fallback_title;
private static readonly CompositeFormat _titleFormat = CompositeFormat.Parse(Resources.settings_fallback_title);
private readonly string _subtitle = Resources.settings_fallback_subtitle;
public FallbackWindowsSettingsItem(Classes.WindowsSettings windowsSettings)
@@ -78,9 +79,9 @@ internal sealed partial class FallbackWindowsSettingsItem : FallbackCommandItem
// We found more than one result. Make our command take
// us to the Windows Settings search page, prepopulated with this search.
var settingsPage = new WindowsSettingsListPage(_windowsSettings, query);
Title = string.Format(CultureInfo.CurrentCulture, _title, query);
Title = _subtitle;
Icon = Icons.WindowsSettingsIcon;
Subtitle = _subtitle;
Subtitle = string.Format(CultureInfo.CurrentCulture, _titleFormat, query);
Command = settingsPage;
return;

View File

@@ -2093,7 +2093,7 @@
<value>Navigate to specific Windows settings</value>
</data>
<data name="settings_fallback_title" xml:space="preserve">
<value>Search for "{0}" in Windows settings</value>
<value>Search for {0}</value>
</data>
<data name="settings_fallback_subtitle" xml:space="preserve">
<value>Search Windows settings for this device</value>

View File

@@ -60,7 +60,7 @@ private:
Hotkey m_editorHotkey = { .key = 0 };
// Whether to use the new WinUI3 editor
bool m_useNewEditor = false;
bool m_useNewEditor = true;
ULONGLONG m_lastHotkeyTime = 0;
@@ -196,11 +196,11 @@ private:
try
{
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
m_useNewEditor = propertiesObject.GetNamedBoolean(JSON_KEY_USE_NEW_EDITOR, false);
m_useNewEditor = propertiesObject.GetNamedBoolean(JSON_KEY_USE_NEW_EDITOR, true);
}
catch (...)
{
Logger::warn("Failed to parse useNewEditor setting, defaulting to false");
Logger::warn("Failed to parse useNewEditor setting, defaulting to true");
}
}
}

View File

@@ -37,6 +37,7 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator.UnitTests
[DataRow("=2^96", "Result value was either too large or too small for a decimal number")]
[DataRow("=+()", "Calculation result is not a valid number (NaN)")]
[DataRow("=[10,10]", "Unsupported use of square brackets")]
[DataRow("=sqrt(-1)", "Complex numbers are not supported")]
[DataRow("=5/0", "Expression contains division by zero")]
[DataRow("=5 / 0", "Expression contains division by zero")]
[DataRow("10+(8*9)/0+7", "Expression contains division by zero")]
@@ -63,6 +64,7 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator.UnitTests
[DataRow("2^96")]
[DataRow("+()")]
[DataRow("[10,10]")]
[DataRow("sqrt(-1)")]
[DataRow("5/0")]
[DataRow("5 / 0")]
[DataRow("10+(8*9)/0+7")]

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text.RegularExpressions;
using Mages.Core;
@@ -124,6 +125,11 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator
return Properties.Resources.wox_plugin_calculator_double_array_returned;
}
if (result is Complex)
{
return Properties.Resources.wox_plugin_calculator_complex_number_returned;
}
return result;
}

View File

@@ -68,6 +68,15 @@ namespace Microsoft.PowerToys.Run.Plugin.Calculator.Properties {
return ResourceManager.GetString("wox_plugin_calculator_calculation_failed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Complex numbers are not supported.
/// </summary>
public static string wox_plugin_calculator_complex_number_returned {
get {
return ResourceManager.GetString("wox_plugin_calculator_complex_number_returned", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy failed, please try later.

View File

@@ -141,6 +141,9 @@
<data name="wox_plugin_calculator_double_array_returned" xml:space="preserve">
<value>Unsupported use of square brackets</value>
</data>
<data name="wox_plugin_calculator_complex_number_returned" xml:space="preserve">
<value>Complex numbers are not supported</value>
</data>
<data name="wox_plugin_calculator_not_covert_to_decimal" xml:space="preserve">
<value>Result value was either too large or too small for a decimal number</value>
</data>
@@ -187,4 +190,4 @@
<value>Gradians</value>
<comment>Text for angle unit.</comment>
</data>
</root>
</root>

View File

@@ -0,0 +1,195 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Common.Services;
namespace PowerDisplay.UnitTests;
[TestClass]
public class CrashDetectionScopeTests
{
private string _tempDir = string.Empty;
private string _lockPath = string.Empty;
[TestInitialize]
public void SetUp()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"pd-scope-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_lockPath = Path.Combine(_tempDir, "discovery.lock");
}
[TestCleanup]
public void TearDown()
{
try
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
catch
{
// best-effort cleanup
}
}
private sealed class FakeProcessExitHook : IProcessExitHook
{
public List<EventHandler> Handlers { get; } = new();
public void Subscribe(EventHandler handler) => Handlers.Add(handler);
public void Unsubscribe(EventHandler handler) => Handlers.Remove(handler);
public void RaiseExit()
{
// Copy to tolerate handlers that mutate the list (e.g. self-unsubscribe).
foreach (var h in Handlers.ToList())
{
h(this, EventArgs.Empty);
}
}
}
[TestMethod]
public void Begin_WritesLockFileAtomically()
{
var hook = new FakeProcessExitHook();
using var scope = CrashDetectionScope.Begin(_lockPath, hook);
Assert.IsTrue(File.Exists(_lockPath), "lock file should exist after Begin");
Assert.IsFalse(File.Exists(_lockPath + ".tmp"), "temp file should not linger");
}
[TestMethod]
public void Begin_SubscribesToProcessExit()
{
var hook = new FakeProcessExitHook();
using var scope = CrashDetectionScope.Begin(_lockPath, hook);
Assert.AreEqual(1, hook.Handlers.Count, "Begin should subscribe exactly one ProcessExit handler");
}
[TestMethod]
public void Dispose_UnsubscribesFromProcessExit()
{
var hook = new FakeProcessExitHook();
var scope = CrashDetectionScope.Begin(_lockPath, hook);
scope.Dispose();
Assert.AreEqual(0, hook.Handlers.Count, "Dispose should unsubscribe the ProcessExit handler");
}
[TestMethod]
public void Dispose_DeletesLockFile()
{
var hook = new FakeProcessExitHook();
var scope = CrashDetectionScope.Begin(_lockPath, hook);
Assert.IsTrue(File.Exists(_lockPath));
scope.Dispose();
Assert.IsFalse(File.Exists(_lockPath), "Dispose should delete the lock file");
}
[TestMethod]
public void ProcessExitFired_BeforeDispose_DeletesLock()
{
var hook = new FakeProcessExitHook();
var scope = CrashDetectionScope.Begin(_lockPath, hook);
Assert.IsTrue(File.Exists(_lockPath));
// Simulate Environment.Exit firing ProcessExit while the scope is still alive
// (i.e. discovery is mid-flight, Dispose hasn't run).
hook.RaiseExit();
Assert.IsFalse(File.Exists(_lockPath), "ProcessExit handler should best-effort delete the lock");
}
[TestMethod]
public void ProcessExitFired_AfterDispose_DoesNothing()
{
var hook = new FakeProcessExitHook();
var scope = CrashDetectionScope.Begin(_lockPath, hook);
scope.Dispose();
Assert.IsFalse(File.Exists(_lockPath));
// Even if a stale handler somehow fires after Dispose, it must not throw.
// (In practice Dispose unsubscribed it; this guards against ordering races.)
hook.RaiseExit();
Assert.IsFalse(File.Exists(_lockPath), "lock should remain deleted");
}
[TestMethod]
public void Dispose_AfterProcessExit_DoesNotThrow()
{
var hook = new FakeProcessExitHook();
var scope = CrashDetectionScope.Begin(_lockPath, hook);
hook.RaiseExit();
Assert.IsFalse(File.Exists(_lockPath));
// Late Dispose (e.g. from a finally block on a still-running thread) must be safe.
scope.Dispose();
Assert.IsFalse(File.Exists(_lockPath));
}
[TestMethod]
public void ProcessExitFired_LockFileMissing_DoesNotThrow()
{
var hook = new FakeProcessExitHook();
var scope = CrashDetectionScope.Begin(_lockPath, hook);
// Simulate the lock being removed by something else first.
File.Delete(_lockPath);
// No exception expected — ProcessExit handlers must not throw or they'd
// disrupt the CLR shutdown sequence.
hook.RaiseExit();
}
[TestMethod]
public void Dispose_IsIdempotent()
{
var hook = new FakeProcessExitHook();
var scope = CrashDetectionScope.Begin(_lockPath, hook);
scope.Dispose();
scope.Dispose();
Assert.AreEqual(0, hook.Handlers.Count);
}
[TestMethod]
public void MultipleScopes_DoNotShareState()
{
var hook1 = new FakeProcessExitHook();
var path1 = Path.Combine(_tempDir, "lock1");
var path2 = Path.Combine(_tempDir, "lock2");
var scope1 = CrashDetectionScope.Begin(path1, hook1);
scope1.Dispose();
var hook2 = new FakeProcessExitHook();
using var scope2 = CrashDetectionScope.Begin(path2, hook2);
Assert.AreEqual(0, hook1.Handlers.Count, "first scope's handler should be unsubscribed");
Assert.AreEqual(1, hook2.Handlers.Count, "second scope should subscribe on its own hook");
Assert.IsFalse(File.Exists(path1));
Assert.IsTrue(File.Exists(path2));
}
}

View File

@@ -20,10 +20,22 @@ namespace PowerDisplay.Common.Services
/// PowerDisplay.exe startup CrashRecovery detects the orphan and disables the module.
///
/// On normal completion or .NET exception, Dispose() removes the lock.
///
/// As a safety net for cooperative shutdowns (Runner's TerminateApp NamedPipe, Terminate
/// named event, tray-quit, Runner-exit detection, PowerToys upgrade), Begin() also
/// subscribes to <see cref="AppDomain.ProcessExit"/>. Those paths currently call
/// <see cref="Environment.Exit"/>, which terminates the process without unwinding
/// background-thread finally blocks — so without this hook the lock would orphan and
/// the next startup would false-positive. ProcessExit fires for Environment.Exit but
/// NOT for FailFast / BSOD / external TerminateProcess, which is exactly the partition
/// we want: cooperative exit → best-effort delete the lock; involuntary kill → leave the
/// lock for Phase 0 to detect.
/// </summary>
public sealed partial class CrashDetectionScope : IDisposable
{
private readonly string _lockPath;
private readonly IProcessExitHook _processExitHook;
private readonly EventHandler _processExitHandler;
private int _disposedFlag;
/// <summary>
@@ -35,9 +47,13 @@ namespace PowerDisplay.Common.Services
/// </summary>
/// <param name="lockPath">Override the default lock path. Defaults to
/// <see cref="PathConstants.DiscoveryLockPath"/>. Test code passes a temp path.</param>
public static CrashDetectionScope Begin(string? lockPath = null)
/// <param name="processExitHook">Override the default <see cref="AppDomain.ProcessExit"/>
/// subscription. Test code passes a fake so it can simulate process exit without
/// terminating the test runner.</param>
public static CrashDetectionScope Begin(string? lockPath = null, IProcessExitHook? processExitHook = null)
{
var path = lockPath ?? PathConstants.DiscoveryLockPath;
var hook = processExitHook ?? AppDomainProcessExitHook.Instance;
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir))
@@ -91,12 +107,39 @@ namespace PowerDisplay.Common.Services
}
Logger.LogInfo($"CrashDetectionScope: lock written at {path}");
return new CrashDetectionScope(path);
var scope = new CrashDetectionScope(path, hook);
hook.Subscribe(scope._processExitHandler);
return scope;
}
private CrashDetectionScope(string lockPath)
private CrashDetectionScope(string lockPath, IProcessExitHook processExitHook)
{
_lockPath = lockPath;
_processExitHook = processExitHook;
_processExitHandler = OnProcessExit;
}
// Fired when the process is exiting cooperatively (Environment.Exit). Best-effort delete
// the lock so the next startup does not false-positive. Must not throw: ProcessExit
// handlers run inside the CLR shutdown sequence and any exception here disrupts it.
// Interlocked guards against a race with Dispose() — only one of the two wins and does
// the delete; the other returns early.
private void OnProcessExit(object? sender, EventArgs e)
{
if (Interlocked.Exchange(ref _disposedFlag, 1) != 0)
{
return;
}
try
{
File.Delete(_lockPath);
}
catch
{
// Worst case: false-positive on next start — same as today's behavior, so this
// hook is strictly an improvement.
}
}
public void Dispose()
@@ -106,6 +149,8 @@ namespace PowerDisplay.Common.Services
return;
}
_processExitHook.Unsubscribe(_processExitHandler);
try
{
File.Delete(_lockPath);

View File

@@ -0,0 +1,35 @@
// 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.Common.Services
{
/// <summary>
/// Abstraction over <see cref="AppDomain.ProcessExit"/> so <see cref="CrashDetectionScope"/>
/// can be unit-tested without invoking real process termination.
/// </summary>
public interface IProcessExitHook
{
void Subscribe(EventHandler handler);
void Unsubscribe(EventHandler handler);
}
/// <summary>
/// Default production implementation that bridges to <see cref="AppDomain.ProcessExit"/>.
/// </summary>
internal sealed class AppDomainProcessExitHook : IProcessExitHook
{
public static readonly AppDomainProcessExitHook Instance = new AppDomainProcessExitHook();
private AppDomainProcessExitHook()
{
}
public void Subscribe(EventHandler handler) => AppDomain.CurrentDomain.ProcessExit += handler;
public void Unsubscribe(EventHandler handler) => AppDomain.CurrentDomain.ProcessExit -= handler;
}
}

View File

@@ -33,7 +33,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public HotkeySettings EditorShortcut { get; set; }
[JsonPropertyName("useNewEditor")]
public bool UseNewEditor { get; set; }
public bool UseNewEditor { get; set; } = true;
public string ToJsonString()
{

View File

@@ -44,9 +44,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("spotlight_mode")]
public BoolProperty SpotlightMode { get; set; }
[JsonPropertyName("ripple_mode")]
public BoolProperty RippleMode { get; set; }
public MouseHighlighterProperties()
{
ActivationShortcut = DefaultActivationShortcut;
@@ -59,7 +56,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
HighlightFadeDurationMs = new IntProperty(250);
AutoActivate = new BoolProperty(false);
SpotlightMode = new BoolProperty(false);
RippleMode = new BoolProperty(false);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -36,7 +36,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.ViewModel
(PowerToysModules.MouseUtils, false),
(PowerToysModules.MouseWithoutBorders, false),
(PowerToysModules.Peek, false),
(PowerToysModules.PowerDisplay, true),
(PowerToysModules.PowerDisplay, false),
(PowerToysModules.PowerRename, false),
(PowerToysModules.Run, false),
(PowerToysModules.QuickAccent, false),

View File

@@ -1,35 +0,0 @@
<ContentDialog
x:Class="Microsoft.PowerToys.Settings.UI.Views.DangerousFeatureWarningDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DefaultButton="Close"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<StackPanel Spacing="12">
<TextBlock
x:Name="WarningHeader"
FontWeight="Bold"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
TextWrapping="Wrap" />
<TextBlock x:Name="WarningDescription" TextWrapping="Wrap" />
<ItemsControl x:Name="WarningList" Margin="20,0,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock Text="{x:Bind}" TextWrapping="Wrap" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
x:Name="WarningConfirm"
FontWeight="SemiBold"
TextWrapping="Wrap" />
</StackPanel>
</ContentDialog>

View File

@@ -1,63 +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 System.Collections.Generic;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.ApplicationModel.Resources;
namespace Microsoft.PowerToys.Settings.UI.Views
{
/// <summary>
/// Confirmation dialog shown when the user enables a feature that can damage the
/// hardware or otherwise leave it in a non-recoverable state. The caller supplies a
/// resource key prefix; the dialog loads
/// "{prefix}_WarningTitle/Header/Description/WarningList_Item{N}/Confirm".
/// Bullets are prepended in code so translators only see the body text; the
/// item loop probes <c>_WarningList_Item1</c>, <c>_Item2</c>, ... until a missing
/// key is hit, so adding a 4th bullet only requires a new resw entry.
/// </summary>
public sealed partial class DangerousFeatureWarningDialog : ContentDialog
{
// Visual decorations are applied in code so translators only see body text.
private const string WarningHeaderPrefix = "⚠️ ";
private const string BulletPrefix = "• ";
// Hard cap on bullet probes; a real dialog never approaches this.
private const int MaxBulletItems = 10;
// Direct ResourceMap handle so the bullet loop can probe for missing keys with
// TryGetValue (returns null) instead of ResourceLoader.GetString (throws
// "NamedResource Not Found").
private static readonly ResourceMap ResourceMap =
new ResourceManager("PowerToys.Settings.pri").MainResourceMap.GetSubtree("Resources");
public DangerousFeatureWarningDialog(string resourceKeyPrefix)
{
InitializeComponent();
var loader = ResourceLoaderInstance.ResourceLoader;
Title = loader.GetString($"{resourceKeyPrefix}_WarningTitle");
WarningHeader.Text = WarningHeaderPrefix + loader.GetString($"{resourceKeyPrefix}_WarningHeader");
WarningDescription.Text = loader.GetString($"{resourceKeyPrefix}_WarningDescription");
WarningConfirm.Text = loader.GetString($"{resourceKeyPrefix}_WarningConfirm");
PrimaryButtonText = loader.GetString("PowerDisplay_Dialog_Enable");
CloseButtonText = loader.GetString("PowerDisplay_Dialog_Cancel");
var items = new List<string>();
for (int i = 1; i <= MaxBulletItems; i++)
{
var candidate = ResourceMap.TryGetValue($"{resourceKeyPrefix}_WarningList_Item{i}");
if (candidate == null || string.IsNullOrEmpty(candidate.ValueAsString))
{
break;
}
items.Add(BulletPrefix + candidate.ValueAsString);
}
WarningList.ItemsSource = items;
}
}
}

View File

@@ -283,10 +283,9 @@
<ComboBox
x:Uid="MouseUtils_MouseHighlighter_SpotlightModeType"
MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind ViewModel.HighlightModeIndex, Mode=TwoWay}">
SelectedIndex="{x:Bind ViewModel.IsSpotlightModeEnabled, Converter={StaticResource ReverseBoolToComboBoxIndexConverter}, Mode=TwoWay}">
<ComboBoxItem x:Uid="HighlightMode_Spotlight_Mode" />
<ComboBoxItem x:Uid="HighlightMode_Circle_Highlight_Mode" />
<ComboBoxItem x:Uid="HighlightMode_Ripple_Mode" />
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="MouseUtils_MouseHighlighter_HighlightRadius">

View File

@@ -35,9 +35,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
Loaded += (s, e) => ViewModel.OnPageLoaded();
}
private async Task<bool> ShowDangerousFeatureDialogAsync(string resourceKeyPrefix)
private async Task<bool> ShowDangerousFeatureDialogAsync(PowerDisplayWarningKind kind)
{
var dialog = new DangerousFeatureWarningDialog(resourceKeyPrefix) { XamlRoot = XamlRoot };
var dialog = new PowerDisplayWarningDialog(kind) { XamlRoot = XamlRoot };
return await dialog.ShowAsync() == ContentDialogResult.Primary;
}
@@ -228,7 +228,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
cb.IsChecked == true,
monitor.EnableColorTemperature,
v => monitor.EnableColorTemperature = v,
"PowerDisplay_ColorTemperature");
PowerDisplayWarningKind.ColorTemperature);
}
private async void EnablePowerState_Click(object sender, RoutedEventArgs e)
@@ -243,7 +243,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
cb.IsChecked == true,
monitor.EnablePowerState,
v => monitor.EnablePowerState = v,
"PowerDisplay_PowerState");
PowerDisplayWarningKind.PowerState);
}
private async void EnableInputSource_Click(object sender, RoutedEventArgs e)
@@ -258,7 +258,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
cb.IsChecked == true,
monitor.EnableInputSource,
v => monitor.EnableInputSource = v,
"PowerDisplay_InputSource");
PowerDisplayWarningKind.InputSource);
}
// Per-monitor CheckBoxes use OneWay binding + Click (Click only fires for real user
@@ -270,7 +270,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
bool desiredValue,
bool currentValue,
Action<bool> commit,
string resourceKeyPrefix)
PowerDisplayWarningKind kind)
{
if (desiredValue == currentValue)
{
@@ -283,7 +283,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return true;
}
if (await ShowDangerousFeatureDialogAsync(resourceKeyPrefix))
if (await ShowDangerousFeatureDialogAsync(kind))
{
commit(true);
return true;

View File

@@ -0,0 +1,21 @@
<ContentDialog
x:Class="Microsoft.PowerToys.Settings.UI.Views.PowerDisplayWarningDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DefaultButton="Close"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<StackPanel Spacing="12">
<controls:InfoBar
x:Name="WarningInfoBar"
IsClosable="False"
IsOpen="True"
Severity="Warning" />
<TextBlock x:Name="WarningBody" TextWrapping="Wrap" />
<HyperlinkButton x:Name="LearnMoreLink" Margin="-12,0,0,0" />
</StackPanel>
</ContentDialog>

View File

@@ -0,0 +1,45 @@
// 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 Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
/// <summary>
/// Shared confirmation dialog for every Power Display action that can damage hardware
/// or leave a monitor in a non-recoverable state. The caller picks a
/// <see cref="PowerDisplayWarningKind"/> and the dialog renders a warning InfoBar
/// (title from <c>PowerDisplay_Warning_{Kind}_InfoBar</c>), a body paragraph with
/// bullets (from <c>PowerDisplay_Warning_{Kind}_Body</c>), and a shared
/// title / learn-more hyperlink / Enable + Cancel buttons.
/// </summary>
public sealed partial class PowerDisplayWarningDialog : ContentDialog
{
// Shared across every variant; not localized.
private const string LearnMoreUrl = "https://aka.ms/powerToysOverview_PowerDisplay_Note";
public PowerDisplayWarningDialog(PowerDisplayWarningKind kind)
{
InitializeComponent();
var loader = ResourceLoaderInstance.ResourceLoader;
// Shared chrome: same title, hyperlink, and buttons on every variant.
Title = loader.GetString("PowerDisplay_Warning_Title");
PrimaryButtonText = loader.GetString("PowerDisplay_Dialog_Enable");
CloseButtonText = loader.GetString("PowerDisplay_Dialog_Cancel");
LearnMoreLink.Content = loader.GetString("PowerDisplay_Warning_LearnMore");
LearnMoreLink.NavigateUri = new Uri(LearnMoreUrl);
// Variant-specific content. The resw key pair is derived from the enum name so
// adding a new warning is one enum value + two resw entries — no code change here.
var prefix = $"PowerDisplay_Warning_{kind}";
WarningInfoBar.Title = loader.GetString($"{prefix}_InfoBar");
WarningBody.Text = loader.GetString($"{prefix}_Body");
}
}
}

View File

@@ -263,9 +263,6 @@
AutomationProperties.AutomationId="WindowingAndLayoutsNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/WindowingAndLayouts.png}"
SelectsOnInvoked="False">
<NavigationViewItem.InfoBadge>
<InfoBadge Style="{StaticResource NewInfoBadge}" />
</NavigationViewItem.InfoBadge>
<NavigationViewItem.MenuItems>
<NavigationViewItem
x:Name="AlwaysOnTopNavigationItem"
@@ -290,11 +287,7 @@
x:Uid="Shell_GrabAndMove"
helpers:NavHelper.NavigateTo="views:GrabAndMovePage"
AutomationProperties.AutomationId="GrabAndMoveNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/GrabAndMove.png}">
<NavigationViewItem.InfoBadge>
<InfoBadge Style="{StaticResource NewInfoBadge}" />
</NavigationViewItem.InfoBadge>
</NavigationViewItem>
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/GrabAndMove.png}" />
<NavigationViewItem
x:Name="WorkspacesNavigationItem"
x:Uid="Shell_Workspaces"
@@ -311,9 +304,6 @@
AutomationProperties.AutomationId="InputOutputNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/InputOutput.png}"
SelectsOnInvoked="False">
<NavigationViewItem.InfoBadge>
<InfoBadge Style="{StaticResource NewInfoBadge}" />
</NavigationViewItem.InfoBadge>
<NavigationViewItem.MenuItems>
<NavigationViewItem
x:Name="KeyboardManagerNavigationItem"
@@ -338,11 +328,7 @@
x:Uid="Shell_PowerDisplay"
helpers:NavHelper.NavigateTo="views:PowerDisplayPage"
AutomationProperties.AutomationId="PowerDisplayNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}">
<NavigationViewItem.InfoBadge>
<InfoBadge Style="{StaticResource NewInfoBadge}" />
</NavigationViewItem.InfoBadge>
</NavigationViewItem>
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}" />
<NavigationViewItem
x:Name="QuickAccentNavigationItem"
x:Uid="Shell_QuickAccent"

View File

@@ -507,7 +507,7 @@ opera.exe</value>
<comment>Product name: Navigation view item name for PowerRename</comment>
</data>
<data name="Shell_ShortcutGuide.Content" xml:space="preserve">
<value>Shortcut Guide V2</value>
<value>Shortcut Guide</value>
<comment>Product name: Navigation view item name for Shortcut Guide</comment>
</data>
<data name="Shell_PowerPreview.Content" xml:space="preserve">
@@ -1129,7 +1129,7 @@ opera.exe</value>
<value>Inactive color</value>
</data>
<data name="ShortcutGuide.ModuleDescription" xml:space="preserve">
<value>Displays an overlay with keyboard shortcuts for Windows and your apps.</value>
<value>Shows an on-screen overlay of keyboard shortcuts for Windows and the active application.</value>
</data>
<data name="ShortcutGuide_ActivationMethod.Header" xml:space="preserve">
<value>Activation method</value>
@@ -1375,7 +1375,7 @@ opera.exe</value>
<comment>do not loc the product name</comment>
</data>
<data name="ShortcutGuide.ModuleTitle" xml:space="preserve">
<value>Shortcut Guide V2</value>
<value>Shortcut Guide</value>
</data>
<data name="General_Repository.Text" xml:space="preserve">
<value>GitHub repository</value>
@@ -2051,13 +2051,7 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
<value>Screen Ruler is a quick and easy way to measure pixels on your screen.</value>
</data>
<data name="Oobe_ShortcutGuide.Description" xml:space="preserve">
<value>The new Shortcut Guide displays keyboard shortcuts for your apps and for the Windows environment. The following apps are included by default (with more to come in the future):
The Microsoft Windows Operating System
Windows Explorer
Notepad
Microsoft PowerToys
The shortcuts always correspond to the most current application/Windows version.</value>
<value>Shortcut Guide displays keyboard shortcuts for Windows and various applications. The shortcuts shown always correspond to the most current application version or Windows version.</value>
</data>
<data name="Oobe_MouseUtils.Description" xml:space="preserve">
<value>A collection of utilities to enhance your mouse.</value>
@@ -2069,7 +2063,7 @@ The shortcuts always correspond to the most current application/Windows version.
Take a look around and explore how some of the PowerToys utilities work and can improve your Windows experience..</value>
</data>
<data name="Oobe_Overview_DescriptionLinkText.Text" xml:space="preserve">
<value>Documentation on Microsoft Learn</value>
<value>Documentation</value>
</data>
<data name="Oobe_Overview_ConfigurePowerToysButton.AutomationProperties.Name" xml:space="preserve">
<value>Configure PowerToys</value>
@@ -2219,7 +2213,7 @@ From there, simply click on one of the supported files in the File Explorer and
<comment>Do not localize this string</comment>
</data>
<data name="Oobe_ShortcutGuide.Title" xml:space="preserve">
<value>Shortcut Guide V2</value>
<value>Shortcut Guide</value>
<comment>Do not localize this string</comment>
</data>
<data name="Oobe_Overview_Title.Text" xml:space="preserve">
@@ -5161,7 +5155,7 @@ The break timer font matches the text font.</value>
<value>No shortcuts to show.</value>
</data>
<data name="HighlightMode.Description" xml:space="preserve">
<value>Highlight the cursor, dim the screen to spotlight it, or pulse a ripple on each click</value>
<value>Highlight the cursor or dim the screen to spotlight it</value>
</data>
<data name="HighlightMode.Header" xml:space="preserve">
<value>Highlight mode</value>
@@ -5172,10 +5166,6 @@ The break timer font matches the text font.</value>
<data name="HighlightMode_Spotlight_Mode.Content" xml:space="preserve">
<value>Spotlight</value>
</data>
<data name="HighlightMode_Ripple_Mode.Content" xml:space="preserve">
<value>Ripple</value>
<comment>Name of the highlight mode that draws an expanding ring pulse on each click.</comment>
</data>
<data name="GeneralPage_EnableDataDiagnosticsText.Text" xml:space="preserve">
<value>Helps us make PowerToys faster, more stable, and better over time</value>
</data>
@@ -5579,23 +5569,24 @@ The break timer font matches the text font.</value>
<data name="PowerDisplay_Monitor_HideMonitor.Content" xml:space="preserve">
<value>Hide monitor</value>
</data>
<data name="PowerDisplay_ColorTemperature_WarningTitle" xml:space="preserve">
<value>Confirm Color Temperature Change</value>
<data name="PowerDisplay_Warning_Title" xml:space="preserve">
<value>Before you continue</value>
<comment>Title shown on every Power Display warning ContentDialog (enable module, color temperature, power state, input source, max compatibility).</comment>
</data>
<data name="PowerDisplay_ColorTemperature_WarningHeader" xml:space="preserve">
<value>Warning: This is a potentially dangerous operation!</value>
<data name="PowerDisplay_Warning_LearnMore" xml:space="preserve">
<value>Learn more about Power Display</value>
<comment>Text for the learn-more hyperlink shown at the bottom of every Power Display warning ContentDialog.</comment>
</data>
<data name="PowerDisplay_ColorTemperature_WarningDescription" xml:space="preserve">
<value>Changing the color temperature setting may cause unpredictable results including:</value>
<data name="PowerDisplay_Warning_ColorTemperature_InfoBar" xml:space="preserve">
<value>Color temperature changes may not be reversible on some monitors</value>
<comment>Title shown in the Warning InfoBar at the top of the color temperature confirmation dialog.</comment>
</data>
<data name="PowerDisplay_ColorTemperature_WarningList_Item1" xml:space="preserve">
<value>Incorrect display colors or other display malfunction</value>
</data>
<data name="PowerDisplay_ColorTemperature_WarningList_Item2" xml:space="preserve">
<value>Settings that cannot be reverted</value>
</data>
<data name="PowerDisplay_ColorTemperature_WarningConfirm" xml:space="preserve">
<value>Do you want to enable color temperature control for this monitor?</value>
<data name="PowerDisplay_Warning_ColorTemperature_Body" xml:space="preserve">
<value>Adjusting color temperature uses DDC/CI to push values your monitor may not fully support.
• Some monitors display incorrect colors or behave unpredictably.
• A small number of monitors retain the new value even after Power Display is closed and may need a factory reset to restore the original profile.</value>
<comment>Body paragraph (with bullets) shown beneath the InfoBar in the color temperature confirmation dialog.</comment>
</data>
<data name="PowerDisplay_CrashDetected_IgnoreButton.Content" xml:space="preserve">
<value>Ignore</value>
@@ -5609,83 +5600,51 @@ The break timer font matches the text font.</value>
<value>PowerDisplay was automatically disabled</value>
<comment>Title of the crash recovery InfoBar shown on PowerDisplay settings page after a detected crash.</comment>
</data>
<data name="PowerDisplay_PowerState_WarningTitle" xml:space="preserve">
<value>Confirm power state control</value>
<data name="PowerDisplay_Warning_PowerState_InfoBar" xml:space="preserve">
<value>Some monitors may need a physical power-cycle to recover</value>
<comment>Title shown in the Warning InfoBar at the top of the power state control confirmation dialog.</comment>
</data>
<data name="PowerDisplay_PowerState_WarningHeader" xml:space="preserve">
<value>Warning: This action may be unsafe.</value>
<data name="PowerDisplay_Warning_PowerState_Body" xml:space="preserve">
<value>Power state control uses DDC/CI to put the monitor into standby or wake it back up.
• Some monitors enter standby but don't wake back up from software — you may need to press the physical power button or reseat the cable.
• Other monitors don't restore the previous state correctly when toggled.</value>
<comment>Body paragraph (with bullets) shown beneath the InfoBar in the power state control confirmation dialog.</comment>
</data>
<data name="PowerDisplay_PowerState_WarningDescription" xml:space="preserve">
<value>Enabling power state control may lead to unexpected behavior, including:</value>
<data name="PowerDisplay_Warning_InputSource_InfoBar" xml:space="preserve">
<value>You may lose software control until you switch back physically</value>
<comment>Title shown in the Warning InfoBar at the top of the input source control confirmation dialog.</comment>
</data>
<data name="PowerDisplay_PowerState_WarningList_Item1" xml:space="preserve">
<value>Monitor may enter standby and not wake via software — recovery may require the physical power button or reseating the cable.</value>
<data name="PowerDisplay_Warning_InputSource_Body" xml:space="preserve">
<value>Input source control switches the monitor to a different input via DDC/CI.
• If the selected input has no signal, the screen will go black until something is connected.
• After switching, software control may be unavailable until you change inputs again using the monitor's physical buttons.
• Not every monitor reliably exposes all of its inputs over DDC/CI.</value>
<comment>Body paragraph (with bullets) shown beneath the InfoBar in the input source control confirmation dialog.</comment>
</data>
<data name="PowerDisplay_PowerState_WarningList_Item2" xml:space="preserve">
<value>Some monitors may not restore the previous state correctly.</value>
<data name="PowerDisplay_Warning_MaxCompatibility_InfoBar" xml:space="preserve">
<value>Probing every VCP feature can destabilize some monitors</value>
<comment>Title shown in the Warning InfoBar at the top of the maximum compatibility mode confirmation dialog.</comment>
</data>
<data name="PowerDisplay_PowerState_WarningConfirm" xml:space="preserve">
<value>Enable power state control for this monitor?</value>
<data name="PowerDisplay_Warning_MaxCompatibility_Body" xml:space="preserve">
<value>Maximum compatibility mode ignores your monitor's capabilities string and probes every supported VCP feature directly. Only enable this if a monitor is missing from Power Display.
• The screen may briefly go black or lose signal during discovery.
• The monitor may respond unpredictably — including unwanted changes to brightness, contrast, or input source.
• The monitor may stop responding to DDC/CI altogether until it's power-cycled.</value>
<comment>Body paragraph (with bullets) shown beneath the InfoBar in the maximum compatibility mode confirmation dialog.</comment>
</data>
<data name="PowerDisplay_InputSource_WarningTitle" xml:space="preserve">
<value>Confirm input source control</value>
<data name="PowerDisplay_Warning_EnableModule_InfoBar" xml:space="preserve">
<value>This may cause a system crash on a small number of monitors</value>
<comment>Title shown in the Warning InfoBar at the top of the Power Display enable confirmation dialog.</comment>
</data>
<data name="PowerDisplay_InputSource_WarningHeader" xml:space="preserve">
<value>Warning: This action may be unsafe.</value>
</data>
<data name="PowerDisplay_InputSource_WarningDescription" xml:space="preserve">
<value>Switching input sources may cause unintended results, including:</value>
</data>
<data name="PowerDisplay_InputSource_WarningList_Item1" xml:space="preserve">
<value>Screen may go black if the selected input has no signal.</value>
</data>
<data name="PowerDisplay_InputSource_WarningList_Item2" xml:space="preserve">
<value>Software control may be lost until you switch back using the monitors physical buttons.</value>
</data>
<data name="PowerDisplay_InputSource_WarningList_Item3" xml:space="preserve">
<value>Some monitors may not reliably expose all input sources.</value>
</data>
<data name="PowerDisplay_InputSource_WarningConfirm" xml:space="preserve">
<value>Enable input source control for this monitor?</value>
</data>
<data name="PowerDisplay_MaxCompatibility_WarningTitle" xml:space="preserve">
<value>Confirm maximum compatibility mode</value>
</data>
<data name="PowerDisplay_MaxCompatibility_WarningHeader" xml:space="preserve">
<value>Warning: This may cause unstable monitor behavior.</value>
</data>
<data name="PowerDisplay_MaxCompatibility_WarningDescription" xml:space="preserve">
<value>Maximum compatibility mode bypasses your monitor's capabilities string and sends a probe for every supported VCP feature. Some monitors do not handle this well, which may cause:</value>
</data>
<data name="PowerDisplay_MaxCompatibility_WarningList_Item1" xml:space="preserve">
<value>The screen may go black or briefly lose signal during discovery.</value>
</data>
<data name="PowerDisplay_MaxCompatibility_WarningList_Item2" xml:space="preserve">
<value>The monitor may respond unpredictably, including unwanted changes to brightness, contrast, or input source.</value>
</data>
<data name="PowerDisplay_MaxCompatibility_WarningList_Item3" xml:space="preserve">
<value>The monitor may stop responding to DDC/CI until it is power-cycled.</value>
</data>
<data name="PowerDisplay_MaxCompatibility_WarningConfirm" xml:space="preserve">
<value>Only enable this if a monitor is missing from Power Display and you fully understand the side effects listed above. Continue?</value>
</data>
<data name="PowerDisplay_EnableModule_WarningTitle" xml:space="preserve">
<value>Enable Power Display?</value>
</data>
<data name="PowerDisplay_EnableModule_WarningHeader" xml:space="preserve">
<value>Warning: This may cause a system crash on a small number of monitors.</value>
</data>
<data name="PowerDisplay_EnableModule_WarningDescription" xml:space="preserve">
<value>Power Display reads each connected monitor's DDC/CI capabilities at startup. On a small number of monitors with malformed capability strings, this read has been observed to trigger a kernel-side bug that causes Windows to bug-check (BSOD). The underlying issue is in Windows, not in Power Display, but the read is what surfaces it. Possible effects:</value>
</data>
<data name="PowerDisplay_EnableModule_WarningList_Item1" xml:space="preserve">
<value>On affected monitors, the system may crash and restart.</value>
</data>
<data name="PowerDisplay_EnableModule_WarningList_Item2" xml:space="preserve">
<value>If a crash is detected, Power Display will auto-disable itself on next launch; you'll need to re-enable the module and dismiss the warning on the settings page each time.</value>
</data>
<data name="PowerDisplay_EnableModule_WarningConfirm" xml:space="preserve">
<value>Only enable Power Display if you accept the risk above. Continue?</value>
<data name="PowerDisplay_Warning_EnableModule_Body" xml:space="preserve">
<value>Power Display reads each connected monitor's DDC/CI capabilities at startup. On a small number of monitors with malformed capability strings, this read can trigger a Windows kernel bug that causes a system crash (BSOD). The underlying issue is in Windows, not in Power Display.
• On affected monitors, the system may crash and restart.
• If a crash is detected, Power Display will automatically disable itself the next time PowerToys launches, and you'll need to re-enable the module each time.</value>
<comment>Body paragraph (with bullets) shown beneath the InfoBar in the Power Display enable confirmation dialog.</comment>
</data>
<data name="PowerDisplay_Dialog_Enable" xml:space="preserve">
<value>Enable</value>

View File

@@ -77,7 +77,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
string alwaysColor = MouseHighlighterSettingsConfig.Properties.AlwaysColor.Value;
_highlighterAlwaysColor = !string.IsNullOrEmpty(alwaysColor) ? alwaysColor : "#00FF0000";
_isSpotlightModeEnabled = MouseHighlighterSettingsConfig.Properties.SpotlightMode.Value;
_isRippleModeEnabled = MouseHighlighterSettingsConfig.Properties.RippleMode.Value;
_highlighterRadius = MouseHighlighterSettingsConfig.Properties.HighlightRadius.Value;
_highlightFadeDelayMs = MouseHighlighterSettingsConfig.Properties.HighlightFadeDelayMs.Value;
@@ -609,64 +608,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool IsRippleModeEnabled
{
get => _isRippleModeEnabled;
set
{
if (_isRippleModeEnabled != value)
{
_isRippleModeEnabled = value;
MouseHighlighterSettingsConfig.Properties.RippleMode.Value = value;
NotifyMouseHighlighterPropertyChanged();
}
}
}
// ComboBox index for the highlight mode selector.
// 0 = Spotlight, 1 = Circle, 2 = Ripple
public int HighlightModeIndex
{
get
{
if (_isSpotlightModeEnabled)
{
return 0;
}
return _isRippleModeEnabled ? 2 : 1;
}
set
{
bool spotlight = value == 0;
bool ripple = value == 2;
bool changed = false;
if (_isSpotlightModeEnabled != spotlight)
{
_isSpotlightModeEnabled = spotlight;
MouseHighlighterSettingsConfig.Properties.SpotlightMode.Value = spotlight;
OnPropertyChanged(nameof(IsSpotlightModeEnabled));
changed = true;
}
if (_isRippleModeEnabled != ripple)
{
_isRippleModeEnabled = ripple;
MouseHighlighterSettingsConfig.Properties.RippleMode.Value = ripple;
OnPropertyChanged(nameof(IsRippleModeEnabled));
changed = true;
}
if (changed)
{
OnPropertyChanged(nameof(HighlightModeIndex));
NotifyMouseHighlighterPropertyChanged();
}
}
}
public int MouseHighlighterRadius
{
get
@@ -1273,7 +1214,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private string _highlighterRightButtonClickColor;
private string _highlighterAlwaysColor;
private bool _isSpotlightModeEnabled;
private bool _isRippleModeEnabled;
private int _highlighterRadius;
private int _highlightFadeDelayMs;
private int _highlightFadeDurationMs;

View File

@@ -162,7 +162,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
try
{
if (await ConfirmDangerousFeatureAsync("PowerDisplay_EnableModule"))
if (await ConfirmDangerousFeatureAsync(PowerDisplayWarningKind.EnableModule))
{
CommitIsEnabled(true);
}
@@ -243,7 +243,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
/// View-supplied confirmation dialog. Default no-op denies all dangerous enables;
/// PowerDisplayPage replaces this in its constructor with a real dialog show.
/// </summary>
public Func<string, Task<bool>> ConfirmDangerousFeatureAsync { get; set; } = _ => Task.FromResult(false);
public Func<PowerDisplayWarningKind, Task<bool>> ConfirmDangerousFeatureAsync { get; set; } = _ => Task.FromResult(false);
// Dangerous toggle. TwoWay-bound to the UI; the setter handles the "ask first"
// gesture entirely inside the ViewModel. Initial-binding push and post-cancel
@@ -278,7 +278,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
try
{
if (await ConfirmDangerousFeatureAsync("PowerDisplay_MaxCompatibility"))
if (await ConfirmDangerousFeatureAsync(PowerDisplayWarningKind.MaxCompatibility))
{
_settings.Properties.MaxCompatibilityMode = true;
NotifySettingsChanged();

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
/// <summary>
/// Identifies which Power Display dangerous-feature confirmation dialog to show.
/// Each value maps to a pair of resw keys
/// (<c>PowerDisplay_Warning_{Kind}_InfoBar</c> + <c>PowerDisplay_Warning_{Kind}_Body</c>)
/// inside the shared <c>PowerDisplayWarningDialog</c> control.
/// </summary>
public enum PowerDisplayWarningKind
{
/// <summary>Shown when the user turns on the Power Display module itself.</summary>
EnableModule,
/// <summary>Shown when the user enables color temperature control for a monitor.</summary>
ColorTemperature,
/// <summary>Shown when the user enables power state control for a monitor.</summary>
PowerState,
/// <summary>Shown when the user enables input source control for a monitor.</summary>
InputSource,
/// <summary>Shown when the user enables maximum compatibility mode.</summary>
MaxCompatibility,
}
}