Compare commits

..

10 Commits

Author SHA1 Message Date
Thanh Nguyen
2be4c4eb46 Fix CursorWrap "Automatically activate on utility startup" setting not persisting (#45210)
## Summary of the Pull Request

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

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

### Problem

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

### Root Causes

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

### Solution

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

### Pattern Reference

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

## Validation Steps Performed

### Build

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

### Manual validation (contributor)

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

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

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

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

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

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

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

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

---

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

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

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

## Detailed Description of the Pull Request / Additional comments
Currently CursorWrap will wrap around the horizontal/vertical edges of
monitors, if the user has more than one monitor the outer edges are used
as wrap targets, if the user only has one monitor (perhaps a laptop)
wrapping might be temporarily disabled until additional external
monitors are added (such as being plugged into a dock or using a USB-C
monitor).

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

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

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-02-05 18:37:10 +08:00
Shawn Yuan
bde2055f26 Fix pipeline build issue when using wasdk 2.0 exp (#45390)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request updates project configuration for two modules by
changing how the Windows App SDK is included. Specifically, it switches
both modules from using a self-contained Windows App SDK deployment to a
framework-dependent deployment. This means the applications will now
rely on the system-installed Windows App SDK rather than bundling it
with the app.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-05 17:04:24 +08:00
moooyo
3336c134dd [PowerDisplay] Add custom vcp code name map and fix some bugs (#45355)
<!-- 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
1. Fix quick access not working bug
2. Add custom value mapping
3. Fix some vcp slider visibility bug

demo for custom vcp value name mapping:
<img width="1399" height="744" alt="image"
src="https://github.com/user-attachments/assets/517e4dbb-409a-4e43-b15a-d0d31e59ce49"
/>
<img width="1379" height="337" alt="image"
src="https://github.com/user-attachments/assets/18f6f389-089c-4441-ad9f-5c45cac53814"
/>
<img width="521" height="1152" alt="image"
src="https://github.com/user-attachments/assets/27b5f796-66fa-4781-b16f-4770bebf3504"
/>
<img width="295" height="808" alt="image"
src="https://github.com/user-attachments/assets/54eaf5b9-5d54-4531-a40b-de3113122715"
/>


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

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

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

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

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
2026-02-05 17:02:55 +08:00
Kai Tao
4c0926d7b7 Doc: Add a dev guideline to make sure codes builds and verified before open a pr (#45419)
<!-- 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
Making sure the codes builds and verified before submitting a pr.

<!-- 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-02-05 16:12:58 +08:00
Shawn Yuan
d9a1c35132 Fix Advanced Paste settings page crash issue (#45207)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
This pull request refactors the `AdvancedPasteAdditionalActions` class
to use private backing fields and custom property accessors for its
action properties. This change allows for better control over property
initialization and ensures that the properties always have valid,
non-null default values.

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

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-05 10:35:59 +08:00
Niels Laute
0259e31d20 Fix contrast issue (#45367)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-05 09:17:13 +08:00
leileizhang
266908c62a [ImageResizer] Fix Image Resizer not working after upgrade on Windows 10 (#45184)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
- Fixes an issue where Image Resizer stops working after upgrading
PowerToys on Windows 10
- Root cause: the PackageIdentityMSIX (sparse app) was not being
properly cleaned up during upgrade

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

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

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

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
1. Install PowerToys version 0.96.1 on Windows 10.
2. Upgrade to version 0.97.1.
3. Run Get-AppxPackage -Name "*Sparse*" in PowerShell to check whether a
Sparse App package is present.
2026-02-05 09:03:48 +08:00
Alex Mihaiuc
6f87e947ff ZoomIt: close the virtual microphone on stop (#45386)
This is not a leak per se, but closing the virtual microphone when
recording stops is a good thing to do.
Also bump ZoomIt's version to 10.1.

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

This is a code quality addition, not a real bug per se - ZoomIt holds
one reference to a virtual microphone stream, thus causing the
notification area (system tray) microphone symbol always show ZoomIt as
"recording" even after stopping a screen recording in ZoomIt.
<!-- 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

It's sufficient to just notice that:

- Audio recording still works.
- The notification area / system tray microphone notification is active
while screen recording.
- That notification disappears after stopping the ZoomIt screen capture
session.
- 
<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-05 09:01:10 +08:00
Gordon Lam
70d84fcb88 chore(claude): add symlinks for Claude Code support to GitHub configs (#45204)
## Summary of the Pull Request

Adds Claude Code support by creating symbolic links under `.claude/`
that point to existing GitHub Copilot configuration files in `.github/`.
This enables Claude Code to use the same AI contributor guidance,
agents, prompts, instructions, and skills without duplicating content.

## Detailed Description of the Pull Request / Additional comments

This PR creates a `.claude/` directory with symbolic links mapping
Claude Code's expected paths to existing GitHub Copilot configurations:

| Claude Code Path | → | GitHub Copilot Source |
|------------------|---|----------------------|
| `.claude/CLAUDE.md` | → | `.github/copilot-instructions.md` |
| `.claude/agents/` | → | `.github/agents/` |
| `.claude/commands/` | → | `.github/prompts/` |
| `.claude/rules/` | → | `.github/instructions/` |
| `.claude/skills/` | → | `.github/skills/` |

**Key benefits:**
- Single source of truth — edits to `.github/` files automatically apply
to Claude Code
- Directory symlinks ensure new files (agents, prompts, skills) are
picked up without updating the mapping
- `.gitattributes` updated with `symlink` hint for `.claude/**`

**Windows users cloning this repo need:**
- Developer Mode enabled, OR admin privileges
- `git config --global core.symlinks true` before cloning

## Validation Steps Performed

- [x] Verified symlinks resolve correctly on Windows (`Get-ChildItem
.claude` shows targets)
- [x] Confirmed content is readable through symlinks (`Get-Content
.claude\CLAUDE.md`)
- [x] Verified Git indexes files as symlinks (mode `120000` in `git
ls-files -s`)
- [x] Confirmed symlink targets stored with forward slashes for
cross-platform compatibility
- [x] No automated tests required — changes are config/symlinks only
with no runtime impact
2026-02-04 08:31:40 -08:00
55 changed files with 1340 additions and 339 deletions

1
.claude/CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
../.github/copilot-instructions.md

1
.claude/agents Symbolic link
View File

@@ -0,0 +1 @@
../.github/agents

1
.claude/commands Symbolic link
View File

@@ -0,0 +1 @@
../.github/prompts

1
.claude/rules Symbolic link
View File

@@ -0,0 +1 @@
../.github/instructions

1
.claude/skills Symbolic link
View File

@@ -0,0 +1 @@
../.github/skills

View File

@@ -68,6 +68,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
- Add the `In progress` label to the issue, if not already present. Also add a `Cost-Small/Medium/Large` estimate and make sure all appropriate labels are set.
- If you are a community contributor, you will not be able to add labels to the issue; in that case just add a comment saying that you have started work on the issue and try to give an estimate for the delivery date.
- If the work item has a medium/large cost, using the markdown task list, list each sub item and update the list with a check mark after completing each sub item.
- **Before opening a PR, ensure your changes build successfully locally and functionality tests pass.** This is especially important for AI-assisted (vibe coding) contributions—always verify AI-generated code works as intended. Exploratory PRs or draft PRs for discussion are exceptions.
- When opening a PR, follow the PR template.
- When you'd like the team to take a look (even if the work is not yet fully complete) mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge.
- When the PR is approved, let the owner of the PR merge it. For community contributions, the reviewer who approved the PR can also merge it.

View File

@@ -147,7 +147,7 @@
<Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UninstallCommandNotFound" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UpgradeCommandNotFound" After="InstallFiles" Condition="WIX_UPGRADE_DETECTED" />
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UninstallServicesTask" After="InstallFinalize" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<!-- TODO: Use to activate embedded MSIX -->
<!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize">

View File

@@ -287,12 +287,4 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::POWER_DISPLAY_TERMINATE_APP_MESSAGE;
}
hstring Constants::MWBToggleEasyMouseEvent()
{
return CommonSharedConstants::MWB_TOGGLE_EASY_MOUSE_EVENT;
}
hstring Constants::MWBReconnectEvent()
{
return CommonSharedConstants::MWB_RECONNECT_EVENT;
}
}

View File

@@ -75,8 +75,6 @@ namespace winrt::PowerToys::Interop::implementation
static hstring PowerDisplayToggleMessage();
static hstring PowerDisplayApplyProfileMessage();
static hstring PowerDisplayTerminateAppMessage();
static hstring MWBToggleEasyMouseEvent();
static hstring MWBReconnectEvent();
};
}

View File

@@ -72,8 +72,6 @@ namespace PowerToys
static String PowerDisplayToggleMessage();
static String PowerDisplayApplyProfileMessage();
static String PowerDisplayTerminateAppMessage();
static String MWBToggleEasyMouseEvent();
static String MWBReconnectEvent();
}
}
}

View File

@@ -174,10 +174,6 @@ namespace CommonSharedConstants
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";
// Path to the events used by MouseWithoutBorders
const wchar_t MWB_TOGGLE_EASY_MOUSE_EVENT[] = L"Local\\PowerToysMWB-ToggleEasyMouseEvent-a9c8d7b6-e5f4-3c2a-1b0d-9e8f7a6b5c4d";
const wchar_t MWB_RECONNECT_EVENT[] = L"Local\\PowerToysMWB-ReconnectEvent-b8d7c6a5-f4e3-2b1c-0a9d-8e7f6a5b4c3d";
// Max DWORD for key code to disable keys.
const DWORD VK_DISABLED = 0x100;
}

View File

@@ -30,7 +30,7 @@
<ApplicationType>Windows Store</ApplicationType>
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
<UseWinUI>true</UseWinUI>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<WindowsAppSDKSelfContained>false</WindowsAppSDKSelfContained>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
</PropertyGroup>
<ItemGroup>

View File

@@ -163,8 +163,22 @@ void CursorWrapCore::UpdateMonitorInfo()
Logger::info(L"======= UPDATE MONITOR INFO END =======");
}
POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode)
POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode, bool disableOnSingleMonitor)
{
// Check if wrapping should be disabled on single monitor
if (disableOnSingleMonitor && m_monitors.size() <= 1)
{
#ifdef _DEBUG
static bool loggedOnce = false;
if (!loggedOnce)
{
OutputDebugStringW(L"[CursorWrap] Single monitor detected - cursor wrapping disabled\n");
loggedOnce = true;
}
#endif
return currentPos;
}
// Check if wrapping should be disabled during drag
if (disableWrapDuringDrag && (GetAsyncKeyState(VK_LBUTTON) & 0x8000))
{

View File

@@ -18,9 +18,11 @@ public:
// Handle mouse move with wrap mode filtering
// wrapMode: 0=Both, 1=VerticalOnly, 2=HorizontalOnly
POINT HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode);
// disableOnSingleMonitor: if true, cursor wrapping is disabled when only one monitor is connected
POINT HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode, bool disableOnSingleMonitor);
const std::vector<MonitorInfo>& GetMonitors() const { return m_monitors; }
size_t GetMonitorCount() const { return m_monitors.size(); }
const MonitorTopology& GetTopology() const { return m_topology; }
private:

View File

@@ -54,6 +54,7 @@ namespace
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t JSON_KEY_DISABLE_WRAP_DURING_DRAG[] = L"disable_wrap_during_drag";
const wchar_t JSON_KEY_WRAP_MODE[] = L"wrap_mode";
const wchar_t JSON_KEY_DISABLE_ON_SINGLE_MONITOR[] = L"disable_cursor_wrap_on_single_monitor";
}
// The PowerToy name that will be shown in the settings.
@@ -80,6 +81,7 @@ private:
bool m_enabled = false;
bool m_autoActivate = false;
bool m_disableWrapDuringDrag = true; // Default to true to prevent wrap during drag
bool m_disableOnSingleMonitor = false; // Default to false
int m_wrapMode = 0; // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
// Mouse hook
@@ -196,6 +198,10 @@ public:
// Start listening for external trigger event so we can invoke the same logic as the activation hotkey.
m_triggerEventHandle = CreateEventW(nullptr, false, false, CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT);
m_terminateEventHandle = CreateEventW(nullptr, false, false, nullptr);
if (m_triggerEventHandle)
{
ResetEvent(m_triggerEventHandle);
}
if (m_triggerEventHandle && m_terminateEventHandle)
{
m_listening = true;
@@ -210,8 +216,16 @@ public:
// Create message window for display change notifications
RegisterForDisplayChanges();
StartMouseHook();
Logger::info("CursorWrap enabled - mouse hook started");
// Only start the mouse hook automatically if auto-activate is enabled
if (m_autoActivate)
{
StartMouseHook();
Logger::info("CursorWrap enabled - mouse hook started (auto-activate on)");
}
else
{
Logger::info("CursorWrap enabled - waiting for activation hotkey (auto-activate off)");
}
while (m_listening)
{
@@ -415,6 +429,21 @@ private:
{
Logger::warn("Failed to initialize CursorWrap wrap mode from settings. Will use default value (0=Both)");
}
try
{
// Parse disable on single monitor
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
if (propertiesObject.HasKey(JSON_KEY_DISABLE_ON_SINGLE_MONITOR))
{
auto disableOnSingleMonitorObject = propertiesObject.GetNamedObject(JSON_KEY_DISABLE_ON_SINGLE_MONITOR);
m_disableOnSingleMonitor = disableOnSingleMonitorObject.GetNamedBoolean(JSON_KEY_VALUE);
}
}
catch (...)
{
Logger::warn("Failed to initialize CursorWrap disable on single monitor from settings. Will use default value (false)");
}
}
else
{
@@ -646,7 +675,8 @@ private:
POINT newPos = g_cursorWrapInstance->m_core.HandleMouseMove(
currentPos,
g_cursorWrapInstance->m_disableWrapDuringDrag,
g_cursorWrapInstance->m_wrapMode);
g_cursorWrapInstance->m_wrapMode,
g_cursorWrapInstance->m_disableOnSingleMonitor);
if (newPos.x != currentPos.x || newPos.y != currentPos.y)
{

View File

@@ -21,7 +21,7 @@
<CppWinRTEnableComponentProjection>false</CppWinRTEnableComponentProjection>
<CppWinRTGenerateWindowsMetadata>false</CppWinRTGenerateWindowsMetadata>
<WindowsAppSdkBootstrapInitialize>false</WindowsAppSdkBootstrapInitialize>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<WindowsAppSDKSelfContained>false</WindowsAppSDKSelfContained>
<WindowsAppSDKVerifyTransitiveDependencies>false</WindowsAppSDKVerifyTransitiveDependencies>
</PropertyGroup>
<ItemGroup>

View File

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

View File

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

View File

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

View File

@@ -311,6 +311,14 @@ void AudioSampleGenerator::Stop()
// Stop the audio graph - no more quantum callbacks will run
m_audioGraph.Stop();
// Close the microphone input node to release the device so Windows no longer
// reports the microphone as in use by ZoomIt.
if (m_audioInputNode)
{
m_audioInputNode.Close();
m_audioInputNode = nullptr;
}
// Mark as stopped
m_started.store(false);

View File

@@ -121,7 +121,7 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN
DEFPUSHBUTTON "OK",IDOK,186,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,243,306,50,14
LTEXT "ZoomIt v10.0",IDC_VERSION,42,7,73,10
LTEXT "ZoomIt v10.1",IDC_VERSION,42,7,73,10
LTEXT "Copyright \251 2006-2026 Mark Russinovich",IDC_COPYRIGHT,42,17,251,8
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
"SysLink",WS_TABSTOP,42,26,150,9

View File

@@ -1,35 +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;
using System.Threading;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToys.Interop;
namespace PowerToysExtension.Commands;
/// <summary>
/// Triggers a reconnection attempt in Mouse Without Borders via the shared event.
/// </summary>
internal sealed partial class MWBReconnectCommand : InvokableCommand
{
public MWBReconnectCommand()
{
Name = "Mouse Without Borders: Reconnect";
}
public override CommandResult Invoke()
{
try
{
using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MWBReconnectEvent());
evt.Set();
return CommandResult.Dismiss();
}
catch (Exception ex)
{
return CommandResult.ShowToast($"Failed to reconnect Mouse Without Borders: {ex.Message}");
}
}
}

View File

@@ -1,35 +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;
using System.Threading;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToys.Interop;
namespace PowerToysExtension.Commands;
/// <summary>
/// Toggles Easy Mouse feature in Mouse Without Borders via the shared event.
/// </summary>
internal sealed partial class ToggleMWBEasyMouseCommand : InvokableCommand
{
public ToggleMWBEasyMouseCommand()
{
Name = "Mouse Without Borders: Toggle Easy Mouse";
}
public override CommandResult Invoke()
{
try
{
using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MWBToggleEasyMouseEvent());
evt.Set();
return CommandResult.Dismiss();
}
catch (Exception ex)
{
return CommandResult.ShowToast($"Failed to toggle Easy Mouse: {ex.Message}");
}
}
}

View File

@@ -13,11 +13,7 @@ internal static class PowerToysResourcesHelper
internal static IconInfo IconFromSettingsIcon(string fileName) => IconHelpers.FromRelativePath($"{SettingsIconRoot}{fileName}");
#if DEBUG
public static IconInfo ProviderIcon() => IconFromSettingsIcon("PowerToys.dark.png");
#else
public static IconInfo ProviderIcon() => IconFromSettingsIcon("PowerToys.png");
#endif
public static IconInfo ModuleIcon(this SettingsWindow module)
{

View File

@@ -30,10 +30,6 @@
<Content Include="..\..\..\..\settings-ui\Settings.UI\Assets\Settings\Icons\*.png" Link="WinUI3Apps\Assets\Settings\Icons\%(Filename)%(Extension)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<!-- Monochrome icons from PowerToys Run Plugin for debug mode differentiation -->
<Content Include="..\..\..\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.PowerToys\Images\PowerToys.dark.png" Link="WinUI3Apps\Assets\Settings\Icons\PowerToys.dark.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>

View File

@@ -24,19 +24,5 @@ internal sealed class MouseWithoutBordersModuleCommandProvider : ModuleCommandPr
Subtitle = Resources.MouseWithoutBorders_Settings_Subtitle,
Icon = icon,
};
yield return new ListItem(new ToggleMWBEasyMouseCommand())
{
Title = Resources.MouseWithoutBorders_ToggleEasyMouse_Title,
Subtitle = Resources.MouseWithoutBorders_ToggleEasyMouse_Subtitle,
Icon = icon,
};
yield return new ListItem(new MWBReconnectCommand())
{
Title = Resources.MouseWithoutBorders_Reconnect_Title,
Subtitle = Resources.MouseWithoutBorders_Reconnect_Subtitle,
Icon = icon,
};
}
}

View File

@@ -14,7 +14,7 @@ internal sealed partial class PowerToysExtensionPage : ListPage
{
public PowerToysExtensionPage()
{
Icon = Helpers.PowerToysResourcesHelper.ProviderIcon();
Icon = Helpers.PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
Title = Resources.PowerToys_DisplayName;
Name = Resources.PowerToysExtension_CommandsName;
}

View File

@@ -15,13 +15,13 @@ internal sealed partial class PowerToysListPage : ListPage
public PowerToysListPage()
{
Icon = PowerToysResourcesHelper.ProviderIcon();
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
Name = Title = Resources.PowerToys_DisplayName;
Id = "com.microsoft.cmdpal.powertoys";
SettingsChangeNotifier.SettingsChanged += OnSettingsChanged;
_empty = new CommandItem()
{
Icon = PowerToysResourcesHelper.ProviderIcon(),
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"),
Title = Resources.PowerToys_NoMatchingModule,
Subtitle = SearchText,
};

View File

@@ -16,7 +16,7 @@ public sealed partial class PowerToysCommandsProvider : CommandProvider
public PowerToysCommandsProvider()
{
DisplayName = Resources.PowerToys_DisplayName;
Icon = PowerToysResourcesHelper.ProviderIcon();
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
}
public override ICommandItem[] TopLevelCommands() =>

View File

@@ -17,7 +17,7 @@ public partial class PowerToysExtensionCommandsProvider : CommandProvider
public PowerToysExtensionCommandsProvider()
{
DisplayName = Resources.PowerToys_DisplayName;
Icon = PowerToysResourcesHelper.ProviderIcon();
Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png");
_commands = [
new CommandItem(new Pages.PowerToysListPage())
{

View File

@@ -996,42 +996,6 @@ namespace PowerToysExtension.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Toggle Easy Mouse.
/// </summary>
internal static string MouseWithoutBorders_ToggleEasyMouse_Title {
get {
return ResourceManager.GetString("MouseWithoutBorders_ToggleEasyMouse_Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Toggle easy mouse switching between machines.
/// </summary>
internal static string MouseWithoutBorders_ToggleEasyMouse_Subtitle {
get {
return ResourceManager.GetString("MouseWithoutBorders_ToggleEasyMouse_Subtitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reconnect.
/// </summary>
internal static string MouseWithoutBorders_Reconnect_Title {
get {
return ResourceManager.GetString("MouseWithoutBorders_Reconnect_Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Reconnect to other machines.
/// </summary>
internal static string MouseWithoutBorders_Reconnect_Subtitle {
get {
return ResourceManager.GetString("MouseWithoutBorders_Reconnect_Subtitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open New+ settings.
/// </summary>

View File

@@ -456,18 +456,6 @@
<data name="MouseWithoutBorders_Settings_Subtitle" xml:space="preserve">
<value>Open Mouse Without Borders settings</value>
</data>
<data name="MouseWithoutBorders_ToggleEasyMouse_Title" xml:space="preserve">
<value>Toggle Easy Mouse</value>
</data>
<data name="MouseWithoutBorders_ToggleEasyMouse_Subtitle" xml:space="preserve">
<value>Toggle Easy Mouse feature on/off</value>
</data>
<data name="MouseWithoutBorders_Reconnect_Title" xml:space="preserve">
<value>Reconnect</value>
</data>
<data name="MouseWithoutBorders_Reconnect_Subtitle" xml:space="preserve">
<value>Reconnect to all machines</value>
</data>
<!-- New+ Module -->
<data name="NewPlus_Settings_Subtitle" xml:space="preserve">
<value>Open New+ settings</value>

View File

@@ -140,7 +140,7 @@
<TextBlock
x:Name="FormatNameTextBlock"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
TextTrimming="CharacterEllipsis" />

View File

@@ -27,13 +27,13 @@
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<!-- <PropertyGroup>
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(CIBuild)'=='true'">
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
</PropertyGroup>
</PropertyGroup> -->
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">

View File

@@ -0,0 +1,88 @@
// 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.Text.Json.Serialization;
using PowerDisplay.Common.Utils;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Represents a custom name mapping for a VCP code value.
/// Used to override the default VCP value names with user-defined names.
/// This class is shared between PowerDisplay app and Settings UI.
/// </summary>
public class CustomVcpValueMapping
{
/// <summary>
/// Gets or sets the VCP code (e.g., 0x14 for color temperature, 0x60 for input source).
/// </summary>
[JsonPropertyName("vcpCode")]
public byte VcpCode { get; set; }
/// <summary>
/// Gets or sets the VCP value to map (e.g., 0x11 for HDMI-1).
/// </summary>
[JsonPropertyName("value")]
public int Value { get; set; }
/// <summary>
/// Gets or sets the custom name to display instead of the default name.
/// </summary>
[JsonPropertyName("customName")]
public string CustomName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether this mapping applies to all monitors.
/// When true, the mapping is applied globally. When false, only applies to TargetMonitorId.
/// </summary>
[JsonPropertyName("applyToAll")]
public bool ApplyToAll { get; set; } = true;
/// <summary>
/// Gets or sets the target monitor ID when ApplyToAll is false.
/// This is the monitor's unique identifier.
/// </summary>
[JsonPropertyName("targetMonitorId")]
public string TargetMonitorId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the target monitor display name (for UI display only, not serialized).
/// </summary>
[JsonIgnore]
public string TargetMonitorName { get; set; } = string.Empty;
/// <summary>
/// Gets the display name for the VCP code (for UI display).
/// Uses VcpNames.GetCodeName() to get the standard MCCS VCP code name.
/// Note: For localized display in Settings UI, use VcpCodeToDisplayNameConverter instead.
/// </summary>
[JsonIgnore]
public string VcpCodeDisplayName => VcpNames.GetCodeName(VcpCode);
/// <summary>
/// Gets the display name for the VCP value (using built-in mapping).
/// </summary>
[JsonIgnore]
public string ValueDisplayName => VcpNames.GetFormattedValueName(VcpCode, Value);
/// <summary>
/// Gets a summary string for display in the UI list.
/// Format: "OriginalValue → CustomName" or "OriginalValue → CustomName (MonitorName)"
/// </summary>
[JsonIgnore]
public string DisplaySummary
{
get
{
var baseSummary = $"{VcpNames.GetValueName(VcpCode, Value) ?? $"0x{Value:X2}"} → {CustomName}";
if (!ApplyToAll && !string.IsNullOrEmpty(TargetMonitorName))
{
return $"{baseSummary} ({TargetMonitorName})";
}
return baseSummary;
}
}
}
}

View File

@@ -2,16 +2,27 @@
// 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.Linq;
using PowerDisplay.Common.Models;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Provides human-readable names for VCP codes and their values based on MCCS v2.2a specification.
/// Combines VCP code names (e.g., 0x10 = "Brightness") and VCP value names (e.g., 0x14:0x05 = "6500K").
/// Supports localization through the LocalizedCodeNameProvider delegate.
/// </summary>
public static class VcpNames
{
/// <summary>
/// Optional delegate to provide localized VCP code names.
/// Set this at application startup to enable localization.
/// The delegate receives a VCP code and should return the localized name, or null to use the default.
/// </summary>
public static Func<byte, string?>? LocalizedCodeNameProvider { get; set; }
/// <summary>
/// VCP code to name mapping
/// </summary>
@@ -237,12 +248,21 @@ namespace PowerDisplay.Common.Utils
};
/// <summary>
/// Get the friendly name for a VCP code
/// Get the friendly name for a VCP code.
/// Uses LocalizedCodeNameProvider if set; falls back to built-in MCCS names if not.
/// </summary>
/// <param name="code">VCP code (e.g., 0x10)</param>
/// <returns>Friendly name, or hex representation if unknown</returns>
public static string GetCodeName(byte code)
{
// Try localized name first
var localizedName = LocalizedCodeNameProvider?.Invoke(code);
if (!string.IsNullOrEmpty(localizedName))
{
return localizedName;
}
// Fallback to built-in MCCS names
return CodeNames.TryGetValue(code, out var name) ? name : $"Unknown (0x{code:X2})";
}
@@ -389,6 +409,16 @@ namespace PowerDisplay.Common.Utils
},
};
/// <summary>
/// Get all known values for a VCP code
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <returns>Dictionary of value to name mappings, or null if no mappings exist</returns>
public static IReadOnlyDictionary<int, string>? GetValueMappings(byte vcpCode)
{
return ValueNames.TryGetValue(vcpCode, out var values) ? values : null;
}
/// <summary>
/// Get human-readable name for a VCP value
/// </summary>
@@ -424,5 +454,59 @@ namespace PowerDisplay.Common.Utils
return $"0x{value:X2}";
}
/// <summary>
/// Get human-readable name for a VCP value with custom mapping support.
/// Custom mappings take priority over built-in mappings.
/// Monitor ID is required to properly filter monitor-specific mappings.
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <param name="value">Value to translate</param>
/// <param name="customMappings">Optional custom mappings that take priority</param>
/// <param name="monitorId">Monitor ID to filter mappings</param>
/// <returns>Name string like "sRGB" or null if unknown</returns>
public static string? GetValueName(byte vcpCode, int value, IEnumerable<CustomVcpValueMapping>? customMappings, string monitorId)
{
// 1. Priority: Check custom mappings first
if (customMappings != null)
{
// Find a matching custom mapping:
// - ApplyToAll = true (global), OR
// - ApplyToAll = false AND TargetMonitorId matches the given monitorId
var custom = customMappings.FirstOrDefault(m =>
m.VcpCode == vcpCode &&
m.Value == value &&
(m.ApplyToAll || (!m.ApplyToAll && m.TargetMonitorId == monitorId)));
if (custom != null && !string.IsNullOrEmpty(custom.CustomName))
{
return custom.CustomName;
}
}
// 2. Fallback to built-in mappings
return GetValueName(vcpCode, value);
}
/// <summary>
/// Get formatted display name for a VCP value with custom mapping support.
/// Custom mappings take priority over built-in mappings.
/// Monitor ID is required to properly filter monitor-specific mappings.
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <param name="value">Value to translate</param>
/// <param name="customMappings">Optional custom mappings that take priority</param>
/// <param name="monitorId">Monitor ID to filter mappings</param>
/// <returns>Formatted string like "sRGB (0x01)" or "0x01" if unknown</returns>
public static string GetFormattedValueName(byte vcpCode, int value, IEnumerable<CustomVcpValueMapping>? customMappings, string monitorId)
{
var name = GetValueName(vcpCode, value, customMappings, monitorId);
if (name != null)
{
return $"{name} (0x{value:X2})";
}
return $"0x{value:X2}";
}
}
}

View File

@@ -125,14 +125,27 @@ namespace PowerDisplay
/// <summary>
/// Called when an existing instance is activated by another process.
/// This happens when EnsureProcessRunning() launches a new process while one is already running.
/// We intentionally don't show the window here - window visibility should only be controlled via:
/// - Toggle event (hotkey, tray icon click, Settings UI Launch button)
/// - Standalone mode startup (handled in OnLaunched)
/// This happens when Quick Access or other launchers start the process while one is already running.
/// We toggle the window to show it - this allows Quick Access launch to work properly.
/// </summary>
private static void OnActivated(object? sender, AppActivationArguments args)
{
Logger.LogInfo("OnActivated: Redirect activation received - window visibility unchanged");
Logger.LogInfo("OnActivated: Redirect activation received - toggling window");
// Toggle the main window on redirect activation
if (_app?.MainWindow is MainWindow mainWindow)
{
// Dispatch to UI thread since OnActivated may be called from a different thread
mainWindow.DispatcherQueue.TryEnqueue(() =>
{
Logger.LogTrace("OnActivated: Toggling window from redirect activation");
mainWindow.ToggleWindow();
});
}
else
{
Logger.LogWarning("OnActivated: MainWindow not available for toggle");
}
}
}
}

View File

@@ -43,6 +43,10 @@ public partial class MainViewModel
// UpdateMonitorList already handles filtering hidden monitors
UpdateMonitorList(_monitorManager.Monitors, isInitialLoad: false);
// Reload UI display settings first (includes custom VCP mappings)
// Must be loaded before ApplyUIConfiguration so names are available for UI refresh
LoadUIDisplaySettings();
// Apply UI configuration changes only (feature visibility toggles, etc.)
// Hardware parameters (brightness, color temperature) are applied via custom actions
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
@@ -51,8 +55,11 @@ public partial class MainViewModel
// Reload profiles in case they were added/updated/deleted in Settings UI
LoadProfiles();
// Reload UI display settings (profile switcher, identify button, color temp switcher)
LoadUIDisplaySettings();
// Notify MonitorViewModels to refresh their custom VCP name displays
foreach (var monitor in Monitors)
{
monitor.RefreshCustomVcpNames();
}
}
catch (Exception ex)
{
@@ -304,7 +311,8 @@ public partial class MainViewModel
}
/// <summary>
/// Apply feature visibility settings to a monitor ViewModel
/// Apply feature visibility settings to a monitor ViewModel.
/// Only shows features that are both enabled by user AND supported by hardware.
/// </summary>
private void ApplyFeatureVisibility(MonitorViewModel monitorVm, PowerDisplaySettings settings)
{
@@ -313,12 +321,13 @@ public partial class MainViewModel
if (monitorSettings != null)
{
monitorVm.ShowContrast = monitorSettings.EnableContrast;
monitorVm.ShowVolume = monitorSettings.EnableVolume;
monitorVm.ShowInputSource = monitorSettings.EnableInputSource;
// Only show features that are both enabled by user AND supported by hardware
monitorVm.ShowContrast = monitorSettings.EnableContrast && monitorVm.SupportsContrast;
monitorVm.ShowVolume = monitorSettings.EnableVolume && monitorVm.SupportsVolume;
monitorVm.ShowInputSource = monitorSettings.EnableInputSource && monitorVm.SupportsInputSource;
monitorVm.ShowRotation = monitorSettings.EnableRotation;
monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature;
monitorVm.ShowPowerState = monitorSettings.EnablePowerState;
monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature && monitorVm.SupportsColorTemperature;
monitorVm.ShowPowerState = monitorSettings.EnablePowerState && monitorVm.SupportsPowerState;
}
}

View File

@@ -163,6 +163,23 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
}
}
// Custom VCP mappings - loaded from settings
private List<CustomVcpValueMapping> _customVcpMappings = new();
/// <summary>
/// Gets or sets the custom VCP value name mappings.
/// These mappings override the default VCP value names for color temperature and input source.
/// </summary>
public List<CustomVcpValueMapping> CustomVcpMappings
{
get => _customVcpMappings;
set
{
_customVcpMappings = value ?? new List<CustomVcpValueMapping>();
OnPropertyChanged();
}
}
public bool IsScanning
{
get => _isScanning;
@@ -389,6 +406,10 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
ShowProfileSwitcher = settings.Properties.ShowProfileSwitcher;
ShowIdentifyMonitorsButton = settings.Properties.ShowIdentifyMonitorsButton;
// Load custom VCP mappings (now using shared type from PowerDisplay.Common.Models)
CustomVcpMappings = settings.Properties.CustomVcpMappings?.ToList() ?? new List<CustomVcpValueMapping>();
Logger.LogInfo($"[Settings] Loaded {CustomVcpMappings.Count} custom VCP mappings");
}
catch (Exception ex)
{

View File

@@ -279,6 +279,16 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
// Advanced control display logic
public bool HasAdvancedControls => ShowContrast || ShowVolume;
/// <summary>
/// Gets a value indicating whether this monitor supports contrast control via VCP 0x12
/// </summary>
public bool SupportsContrast => _monitor.SupportsContrast;
/// <summary>
/// Gets a value indicating whether this monitor supports volume control via VCP 0x62
/// </summary>
public bool SupportsVolume => _monitor.SupportsVolume;
public bool ShowContrast
{
get => _showContrast;
@@ -456,8 +466,10 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// <summary>
/// Gets human-readable color temperature preset name (e.g., "6500K", "sRGB")
/// Uses custom mappings if available; falls back to built-in names if not.
/// </summary>
public string ColorTemperaturePresetName => _monitor.ColorTemperaturePresetName;
public string ColorTemperaturePresetName =>
Common.Utils.VcpNames.GetFormattedValueName(0x14, _monitor.CurrentColorTemperature, _mainViewModel?.CustomVcpMappings, _monitor.Id);
/// <summary>
/// Gets a value indicating whether this monitor supports color temperature via VCP 0x14
@@ -537,7 +549,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_availableColorPresets = presetValues.Select(value => new ColorTemperatureItem
{
VcpValue = value,
DisplayName = Common.Utils.VcpNames.GetFormattedValueName(0x14, value),
DisplayName = Common.Utils.VcpNames.GetFormattedValueName(0x14, value, _mainViewModel?.CustomVcpMappings, _monitor.Id),
IsSelected = value == _monitor.CurrentColorTemperature,
MonitorId = _monitor.Id,
}).ToList();
@@ -557,8 +569,11 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// <summary>
/// Gets human-readable current input source name (e.g., "HDMI-1", "DisplayPort-1")
/// Uses custom mappings if available; falls back to built-in names if not.
/// </summary>
public string CurrentInputSourceName => _monitor.InputSourceName;
public string CurrentInputSourceName =>
Common.Utils.VcpNames.GetValueName(0x60, _monitor.CurrentInputSource, _mainViewModel?.CustomVcpMappings, _monitor.Id)
?? $"Source 0x{_monitor.CurrentInputSource:X2}";
private List<InputSourceItem>? _availableInputSources;
@@ -593,7 +608,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_availableInputSources = supportedSources.Select(value => new InputSourceItem
{
Value = value,
Name = Common.Utils.VcpNames.GetValueName(0x60, value) ?? $"Source 0x{value:X2}",
Name = Common.Utils.VcpNames.GetValueName(0x60, value, _mainViewModel?.CustomVcpMappings, _monitor.Id) ?? $"Source 0x{value:X2}",
SelectionVisibility = value == _monitor.CurrentInputSource ? Visibility.Visible : Visibility.Collapsed,
MonitorId = _monitor.Id,
}).ToList();
@@ -601,6 +616,23 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
OnPropertyChanged(nameof(AvailableInputSources));
}
/// <summary>
/// Refresh custom VCP name displays after settings change.
/// Called when CustomVcpMappings is updated from Settings UI.
/// </summary>
public void RefreshCustomVcpNames()
{
// Refresh color temperature names
OnPropertyChanged(nameof(ColorTemperaturePresetName));
_availableColorPresets = null; // Force rebuild with new custom names
OnPropertyChanged(nameof(AvailableColorPresets));
// Refresh input source names
OnPropertyChanged(nameof(CurrentInputSourceName));
_availableInputSources = null; // Force rebuild with new custom names
OnPropertyChanged(nameof(AvailableInputSources));
}
/// <summary>
/// Set input source for this monitor
/// </summary>

View File

@@ -52,8 +52,11 @@ void PowerDisplayProcessManager::send_message(const std::wstring& message_type,
{
submit_task([this, message_type, message_arg] {
// Ensure process is running before sending message
if (!is_process_running() && m_enabled)
// If process is not running, enable and start it - this allows Quick Access launch
// to work even when the module was not previously enabled
if (!is_process_running())
{
m_enabled = true;
refresh();
}
send_named_pipe_message(message_type, message_arg);

View File

@@ -9,6 +9,8 @@
#include <common/utils/winapi_error.h>
#include <common/utils/logger_helper.h>
#include <common/utils/resources.h>
#include <thread>
#include <atomic>
#include "resource.h"
@@ -48,6 +50,11 @@ private:
HANDLE m_hRefreshEvent = nullptr;
HANDLE m_hSendSettingsTelemetryEvent = nullptr;
// Toggle event handle and listener thread for Quick Access support
HANDLE m_hToggleEvent = nullptr;
HANDLE m_hStopEvent = nullptr; // Manual-reset event to signal thread termination
std::thread m_toggleEventThread;
public:
PowerDisplayModule()
{
@@ -62,16 +69,29 @@ public:
m_hSendSettingsTelemetryEvent = CreateDefaultEvent(CommonSharedConstants::POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT);
Logger::trace(L"Created SEND_SETTINGS_TELEMETRY_EVENT: handle={}", reinterpret_cast<void*>(m_hSendSettingsTelemetryEvent));
if (!m_hRefreshEvent || !m_hSendSettingsTelemetryEvent)
// Create Toggle event for Quick Access support
// This allows Quick Access to launch PowerDisplay even when module is not enabled
m_hToggleEvent = CreateDefaultEvent(CommonSharedConstants::TOGGLE_POWER_DISPLAY_EVENT);
Logger::trace(L"Created TOGGLE_EVENT: handle={}", reinterpret_cast<void*>(m_hToggleEvent));
// Create manual-reset stop event for clean thread termination
m_hStopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
Logger::trace(L"Created STOP_EVENT: handle={}", reinterpret_cast<void*>(m_hStopEvent));
if (!m_hRefreshEvent || !m_hSendSettingsTelemetryEvent || !m_hToggleEvent || !m_hStopEvent)
{
Logger::error(L"Failed to create one or more event handles: Refresh={}, SettingsTelemetry={}",
Logger::error(L"Failed to create one or more event handles: Refresh={}, SettingsTelemetry={}, Toggle={}",
reinterpret_cast<void*>(m_hRefreshEvent),
reinterpret_cast<void*>(m_hSendSettingsTelemetryEvent));
reinterpret_cast<void*>(m_hSendSettingsTelemetryEvent),
reinterpret_cast<void*>(m_hToggleEvent));
}
else
{
Logger::info(L"All Windows Events created successfully");
}
// Start toggle event listener thread for Quick Access support
StartToggleEventListener();
}
~PowerDisplayModule()
@@ -81,6 +101,9 @@ public:
disable();
}
// Stop toggle event listener thread
StopToggleEventListener();
// Clean up event handles
if (m_hRefreshEvent)
{
@@ -92,6 +115,99 @@ public:
CloseHandle(m_hSendSettingsTelemetryEvent);
m_hSendSettingsTelemetryEvent = nullptr;
}
if (m_hToggleEvent)
{
CloseHandle(m_hToggleEvent);
m_hToggleEvent = nullptr;
}
if (m_hStopEvent)
{
CloseHandle(m_hStopEvent);
m_hStopEvent = nullptr;
}
}
void StartToggleEventListener()
{
if (!m_hToggleEvent || !m_hStopEvent)
{
return;
}
// Reset stop event before starting thread
ResetEvent(m_hStopEvent);
m_toggleEventThread = std::thread([this]() {
Logger::info(L"Toggle event listener thread started");
HANDLE handles[] = { m_hToggleEvent, m_hStopEvent };
constexpr DWORD TOGGLE_EVENT_INDEX = 0;
constexpr DWORD STOP_EVENT_INDEX = 1;
while (true)
{
// Wait indefinitely for either toggle event or stop event
DWORD result = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
if (result == WAIT_OBJECT_0 + TOGGLE_EVENT_INDEX)
{
Logger::trace(L"Toggle event received");
TogglePowerDisplay();
}
else if (result == WAIT_OBJECT_0 + STOP_EVENT_INDEX)
{
// Stop event signaled - exit the loop
Logger::trace(L"Stop event received, exiting toggle listener");
break;
}
else
{
// WAIT_FAILED or unexpected result
Logger::warn(L"WaitForMultipleObjects returned unexpected result: {}", result);
break;
}
}
Logger::info(L"Toggle event listener thread stopped");
});
}
void StopToggleEventListener()
{
if (m_hStopEvent)
{
// Signal the stop event to wake up the waiting thread
SetEvent(m_hStopEvent);
}
if (m_toggleEventThread.joinable())
{
m_toggleEventThread.join();
}
}
/// <summary>
/// Toggle PowerDisplay window visibility.
/// If process is running, launches again to trigger redirect activation (OnActivated handles toggle).
/// If process is not running, starts it via Named Pipe and sends toggle message.
/// </summary>
void TogglePowerDisplay()
{
if (m_processManager.is_running())
{
// Process running - launch to trigger single instance redirect, OnActivated will toggle
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = SEE_MASK_FLAG_NO_UI;
sei.lpFile = L"WinUI3Apps\\PowerToys.PowerDisplay.exe";
sei.nShow = SW_SHOWNORMAL;
ShellExecuteExW(&sei);
}
else
{
// Process not running - start and send toggle via Named Pipe
m_processManager.send_message(CommonSharedConstants::POWER_DISPLAY_TOGGLE_MESSAGE);
}
Trace::ActivatePowerDisplay();
}
virtual void destroy() override
@@ -135,10 +251,7 @@ public:
if (action_object.get_name() == L"Launch")
{
Logger::trace(L"Launch action received");
// Send Toggle message via Named Pipe (will start process if needed)
m_processManager.send_message(CommonSharedConstants::POWER_DISPLAY_TOGGLE_MESSAGE);
Trace::ActivatePowerDisplay();
TogglePowerDisplay();
}
else if (action_object.get_name() == L"RefreshMonitors")
{

View File

@@ -119,6 +119,13 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
eventHandle.Set();
}
return true;
case ModuleType.PowerDisplay:
using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.TogglePowerDisplayEvent()))
{
eventHandle.Set();
}
return true;
default:
return false;

View File

@@ -10,6 +10,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed class AdvancedPasteAdditionalActions
{
private AdvancedPasteAdditionalAction _imageToText = new();
private AdvancedPastePasteAsFileAction _pasteAsFile = new();
private AdvancedPasteTranscodeAction _transcode = new();
public static class PropertyNames
{
public const string ImageToText = "image-to-text";
@@ -18,13 +22,25 @@ public sealed class AdvancedPasteAdditionalActions
}
[JsonPropertyName(PropertyNames.ImageToText)]
public AdvancedPasteAdditionalAction ImageToText { get; init; } = new();
public AdvancedPasteAdditionalAction ImageToText
{
get => _imageToText;
init => _imageToText = value ?? new();
}
[JsonPropertyName(PropertyNames.PasteAsFile)]
public AdvancedPastePasteAsFileAction PasteAsFile { get; init; } = new();
public AdvancedPastePasteAsFileAction PasteAsFile
{
get => _pasteAsFile;
init => _pasteAsFile = value ?? new();
}
[JsonPropertyName(PropertyNames.Transcode)]
public AdvancedPasteTranscodeAction Transcode { get; init; } = new();
public AdvancedPasteTranscodeAction Transcode
{
get => _transcode;
init => _transcode = value ?? new();
}
public IEnumerable<IAdvancedPasteAction> GetAllActions()
{

View File

@@ -25,12 +25,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("wrap_mode")]
public IntProperty WrapMode { get; set; }
[JsonPropertyName("disable_cursor_wrap_on_single_monitor")]
public BoolProperty DisableCursorWrapOnSingleMonitor { get; set; }
public CursorWrapProperties()
{
ActivationShortcut = DefaultActivationShortcut;
AutoActivate = new BoolProperty(false);
DisableWrapDuringDrag = new BoolProperty(true);
WrapMode = new IntProperty(0); // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
DisableCursorWrapOnSingleMonitor = new BoolProperty(false);
}
}
}

View File

@@ -56,6 +56,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library
settingsUpgraded = true;
}
// Add DisableCursorWrapOnSingleMonitor property if it doesn't exist (for users upgrading from older versions)
if (Properties.DisableCursorWrapOnSingleMonitor == null)
{
Properties.DisableCursorWrapOnSingleMonitor = new BoolProperty(false); // Default to false
settingsUpgraded = true;
}
return settingsUpgraded;
}
}

View File

@@ -23,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
ShowSystemTrayIcon = true;
ShowProfileSwitcher = true;
ShowIdentifyMonitorsButton = true;
CustomVcpMappings = new List<CustomVcpValueMapping>();
// Note: saved_monitor_settings has been moved to monitor_state.json
// which is managed separately by PowerDisplay app
@@ -61,5 +62,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
/// </summary>
[JsonPropertyName("show_identify_monitors_button")]
public bool ShowIdentifyMonitorsButton { get; set; }
/// <summary>
/// Gets or sets custom VCP value name mappings shared across all monitors.
/// Allows users to define custom names for color temperature presets and input sources.
/// </summary>
[JsonPropertyName("custom_vcp_mappings")]
public List<CustomVcpValueMapping> CustomVcpMappings { get; set; }
}
}

View File

@@ -0,0 +1,75 @@
<ContentDialog
x:Class="Microsoft.PowerToys.Settings.UI.Views.CustomVcpMappingEditorDialog"
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"
Width="400"
MinWidth="400"
DefaultButton="Primary"
IsPrimaryButtonEnabled="{x:Bind CanSave, Mode=OneWay}"
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<StackPanel MinWidth="350" Spacing="16">
<!-- VCP Code Selection -->
<ComboBox
x:Name="VcpCodeComboBox"
x:Uid="PowerDisplay_CustomMappingEditor_VcpCode"
HorizontalAlignment="Stretch"
SelectionChanged="VcpCodeComboBox_SelectionChanged">
<ComboBoxItem x:Name="VcpCodeItem_0x14" Tag="20" />
<ComboBoxItem x:Name="VcpCodeItem_0x60" Tag="96" />
</ComboBox>
<!-- Value Selection from monitors -->
<StackPanel Spacing="8">
<ComboBox
x:Name="ValueComboBox"
x:Uid="PowerDisplay_CustomMappingEditor_ValueComboBox"
HorizontalAlignment="Stretch"
DisplayMemberPath="DisplayName"
ItemsSource="{x:Bind AvailableValues, Mode=OneWay}"
SelectedValuePath="Value"
SelectionChanged="ValueComboBox_SelectionChanged" />
<!-- Custom Value Input (shown when "Custom value" is selected) -->
<TextBox
x:Name="CustomValueTextBox"
x:Uid="PowerDisplay_CustomMappingEditor_CustomValueInput"
HorizontalAlignment="Stretch"
PlaceholderText="0x11"
TextChanged="CustomValueTextBox_TextChanged"
Visibility="{x:Bind ShowCustomValueInput, Mode=OneWay}" />
</StackPanel>
<!-- Custom Name Input -->
<TextBox
x:Name="CustomNameTextBox"
x:Uid="PowerDisplay_CustomMappingEditor_CustomName"
HorizontalAlignment="Stretch"
MaxLength="50"
TextChanged="CustomNameTextBox_TextChanged" />
<!-- Apply Scope -->
<StackPanel Spacing="8">
<ToggleSwitch
x:Name="ApplyToAllToggle"
x:Uid="PowerDisplay_CustomMappingEditor_ApplyToAll"
IsOn="True"
Toggled="ApplyToAllToggle_Toggled" />
<!-- Monitor Selection (shown when ApplyToAll is off) -->
<ComboBox
x:Name="MonitorComboBox"
x:Uid="PowerDisplay_CustomMappingEditor_SelectMonitor"
HorizontalAlignment="Stretch"
DisplayMemberPath="DisplayName"
ItemsSource="{x:Bind AvailableMonitors, Mode=OneWay}"
SelectedValuePath="Id"
SelectionChanged="MonitorComboBox_SelectionChanged"
Visibility="{x:Bind ShowMonitorSelector, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</ContentDialog>

View File

@@ -0,0 +1,421 @@
// 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
namespace Microsoft.PowerToys.Settings.UI.Views
{
/// <summary>
/// Dialog for creating/editing custom VCP value name mappings
/// </summary>
public sealed partial class CustomVcpMappingEditorDialog : ContentDialog, INotifyPropertyChanged
{
/// <summary>
/// Special value to indicate "Custom value" option in the ComboBox
/// </summary>
private const int CustomValueMarker = -1;
/// <summary>
/// Represents a selectable VCP value item in the Value ComboBox
/// </summary>
public class VcpValueItem
{
public int Value { get; set; }
public string DisplayName { get; set; } = string.Empty;
public bool IsCustomOption => Value == CustomValueMarker;
}
/// <summary>
/// Represents a selectable monitor item in the Monitor ComboBox
/// </summary>
public class MonitorItem
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
}
private readonly IEnumerable<MonitorInfo>? _monitors;
private ObservableCollection<VcpValueItem> _availableValues = new();
private ObservableCollection<MonitorItem> _availableMonitors = new();
private byte _selectedVcpCode;
private int _selectedValue;
private string _customName = string.Empty;
private bool _canSave;
private bool _showCustomValueInput;
private bool _showMonitorSelector;
private int _customValueParsed;
private bool _applyToAll = true;
private string _selectedMonitorId = string.Empty;
private string _selectedMonitorName = string.Empty;
public CustomVcpMappingEditorDialog(IEnumerable<MonitorInfo>? monitors)
{
_monitors = monitors;
this.InitializeComponent();
// Set localized strings for ContentDialog
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
Title = resourceLoader.GetString("PowerDisplay_CustomMappingEditor_Title");
PrimaryButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Save");
CloseButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Cancel");
// Set VCP code ComboBox items content dynamically using localized names
VcpCodeItem_0x14.Content = GetFormattedVcpCodeName(resourceLoader, 0x14);
VcpCodeItem_0x60.Content = GetFormattedVcpCodeName(resourceLoader, 0x60);
// Populate monitor list
PopulateMonitorList();
// Default to Color Temperature (0x14)
VcpCodeComboBox.SelectedIndex = 0;
}
/// <summary>
/// Gets the result mapping after dialog closes with Primary button
/// </summary>
public CustomVcpValueMapping? ResultMapping { get; private set; }
/// <summary>
/// Gets the available values for the selected VCP code
/// </summary>
public ObservableCollection<VcpValueItem> AvailableValues
{
get => _availableValues;
private set
{
_availableValues = value;
OnPropertyChanged();
}
}
/// <summary>
/// Gets the available monitors for selection
/// </summary>
public ObservableCollection<MonitorItem> AvailableMonitors
{
get => _availableMonitors;
private set
{
_availableMonitors = value;
OnPropertyChanged();
}
}
/// <summary>
/// Gets a value indicating whether the dialog can be saved
/// </summary>
public bool CanSave
{
get => _canSave;
private set
{
if (_canSave != value)
{
_canSave = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets a value indicating whether to show the custom value input TextBox
/// </summary>
public Visibility ShowCustomValueInput => _showCustomValueInput ? Visibility.Visible : Visibility.Collapsed;
/// <summary>
/// Gets a value indicating whether to show the monitor selector ComboBox
/// </summary>
public Visibility ShowMonitorSelector => _showMonitorSelector ? Visibility.Visible : Visibility.Collapsed;
private void SetShowCustomValueInput(bool value)
{
if (_showCustomValueInput != value)
{
_showCustomValueInput = value;
OnPropertyChanged(nameof(ShowCustomValueInput));
}
}
private void SetShowMonitorSelector(bool value)
{
if (_showMonitorSelector != value)
{
_showMonitorSelector = value;
OnPropertyChanged(nameof(ShowMonitorSelector));
}
}
private void PopulateMonitorList()
{
AvailableMonitors = new ObservableCollection<MonitorItem>(
_monitors?.Select(m => new MonitorItem { Id = m.Id, DisplayName = m.DisplayName })
?? Enumerable.Empty<MonitorItem>());
if (AvailableMonitors.Count > 0)
{
MonitorComboBox.SelectedIndex = 0;
}
}
/// <summary>
/// Pre-fill the dialog with existing mapping data for editing
/// </summary>
public void PreFillMapping(CustomVcpValueMapping mapping)
{
if (mapping is null)
{
return;
}
// Select the VCP code
VcpCodeComboBox.SelectedIndex = mapping.VcpCode == 0x14 ? 0 : 1;
// Populate values for the selected VCP code
PopulateValuesForVcpCode(mapping.VcpCode);
// Try to select the value in the ComboBox
var matchingItem = AvailableValues.FirstOrDefault(v => !v.IsCustomOption && v.Value == mapping.Value);
if (matchingItem is not null)
{
ValueComboBox.SelectedItem = matchingItem;
}
else
{
// Value not found in list, select "Custom value" option and fill the TextBox
ValueComboBox.SelectedItem = AvailableValues.FirstOrDefault(v => v.IsCustomOption);
CustomValueTextBox.Text = $"0x{mapping.Value:X2}";
_customValueParsed = mapping.Value;
}
// Set the custom name
CustomNameTextBox.Text = mapping.CustomName;
_customName = mapping.CustomName;
// Set apply scope
_applyToAll = mapping.ApplyToAll;
ApplyToAllToggle.IsOn = mapping.ApplyToAll;
SetShowMonitorSelector(!mapping.ApplyToAll);
// Select the target monitor if not applying to all
if (!mapping.ApplyToAll && !string.IsNullOrEmpty(mapping.TargetMonitorId))
{
var targetMonitor = AvailableMonitors.FirstOrDefault(m => m.Id == mapping.TargetMonitorId);
if (targetMonitor is not null)
{
MonitorComboBox.SelectedItem = targetMonitor;
_selectedMonitorId = targetMonitor.Id;
_selectedMonitorName = targetMonitor.DisplayName;
}
}
UpdateCanSave();
}
private void VcpCodeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (VcpCodeComboBox.SelectedItem is ComboBoxItem selectedItem &&
selectedItem.Tag is string tagValue &&
byte.TryParse(tagValue, out byte vcpCode))
{
_selectedVcpCode = vcpCode;
PopulateValuesForVcpCode(vcpCode);
UpdateCanSave();
}
}
private void PopulateValuesForVcpCode(byte vcpCode)
{
var values = new ObservableCollection<VcpValueItem>();
var seenValues = new HashSet<int>();
// Collect values from all monitors
if (_monitors is not null)
{
foreach (var monitor in _monitors)
{
if (monitor.VcpCodesFormatted is null)
{
continue;
}
// Find the VCP code entry
var vcpEntry = monitor.VcpCodesFormatted.FirstOrDefault(v =>
!string.IsNullOrEmpty(v.Code) &&
TryParseHexCode(v.Code, out int code) &&
code == vcpCode);
if (vcpEntry?.ValueList is null)
{
continue;
}
// Add each value from this monitor
foreach (var valueInfo in vcpEntry.ValueList)
{
if (TryParseHexCode(valueInfo.Value, out int vcpValue) && !seenValues.Contains(vcpValue))
{
seenValues.Add(vcpValue);
var displayName = !string.IsNullOrEmpty(valueInfo.Name)
? $"{valueInfo.Name} (0x{vcpValue:X2})"
: VcpNames.GetFormattedValueName(vcpCode, vcpValue);
values.Add(new VcpValueItem
{
Value = vcpValue,
DisplayName = displayName,
});
}
}
}
}
// If no values found from monitors, fall back to built-in values from VcpNames
if (values.Count == 0)
{
var builtInValues = VcpNames.GetValueMappings(vcpCode);
if (builtInValues is not null)
{
foreach (var kvp in builtInValues)
{
values.Add(new VcpValueItem
{
Value = kvp.Key,
DisplayName = $"{kvp.Value} (0x{kvp.Key:X2})",
});
}
}
}
// Sort by value
var sortedValues = new ObservableCollection<VcpValueItem>(values.OrderBy(v => v.Value));
// Add "Custom value" option at the end
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
sortedValues.Add(new VcpValueItem
{
Value = CustomValueMarker,
DisplayName = resourceLoader.GetString("PowerDisplay_CustomMappingEditor_CustomValueOption"),
});
AvailableValues = sortedValues;
// Select first item if available
if (sortedValues.Count > 0)
{
ValueComboBox.SelectedIndex = 0;
}
}
private static bool TryParseHexCode(string? hex, out int result)
{
result = 0;
if (string.IsNullOrEmpty(hex))
{
return false;
}
var cleanHex = hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? hex[2..] : hex;
return int.TryParse(cleanHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out result);
}
private static string GetFormattedVcpCodeName(Windows.ApplicationModel.Resources.ResourceLoader resourceLoader, byte vcpCode)
{
var resourceKey = $"PowerDisplay_VcpCode_Name_0x{vcpCode:X2}";
var localizedName = resourceLoader.GetString(resourceKey);
var name = string.IsNullOrEmpty(localizedName) ? VcpNames.GetCodeName(vcpCode) : localizedName;
return $"{name} (0x{vcpCode:X2})";
}
private void ValueComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ValueComboBox.SelectedItem is VcpValueItem selectedItem)
{
SetShowCustomValueInput(selectedItem.IsCustomOption);
_selectedValue = selectedItem.IsCustomOption ? 0 : selectedItem.Value;
UpdateCanSave();
}
}
private void CustomValueTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
_customValueParsed = TryParseHexCode(CustomValueTextBox.Text?.Trim(), out int parsed) ? parsed : 0;
UpdateCanSave();
}
private void CustomNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
_customName = CustomNameTextBox.Text?.Trim() ?? string.Empty;
UpdateCanSave();
}
private void ApplyToAllToggle_Toggled(object sender, RoutedEventArgs e)
{
_applyToAll = ApplyToAllToggle.IsOn;
SetShowMonitorSelector(!_applyToAll);
UpdateCanSave();
}
private void MonitorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (MonitorComboBox.SelectedItem is MonitorItem selectedMonitor)
{
_selectedMonitorId = selectedMonitor.Id;
_selectedMonitorName = selectedMonitor.DisplayName;
UpdateCanSave();
}
}
private void UpdateCanSave()
{
var hasValidValue = _showCustomValueInput
? _customValueParsed > 0
: ValueComboBox.SelectedItem is VcpValueItem item && !item.IsCustomOption;
CanSave = _selectedVcpCode > 0 &&
hasValidValue &&
!string.IsNullOrWhiteSpace(_customName) &&
(_applyToAll || !string.IsNullOrEmpty(_selectedMonitorId));
}
private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (CanSave)
{
int finalValue = _showCustomValueInput ? _customValueParsed : _selectedValue;
ResultMapping = new CustomVcpValueMapping
{
VcpCode = _selectedVcpCode,
Value = finalValue,
CustomName = _customName,
ApplyToAll = _applyToAll,
TargetMonitorId = _applyToAll ? string.Empty : _selectedMonitorId,
TargetMonitorName = _applyToAll ? string.Empty : _selectedMonitorName,
};
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -54,6 +54,9 @@
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_HorizontalOnly" />
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}">
<CheckBox x:Uid="MouseUtils_CursorWrap_DisableOnSingleMonitor" IsChecked="{x:Bind ViewModel.CursorWrapDisableOnSingleMonitor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>

View File

@@ -63,11 +63,51 @@
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<!-- Custom VCP Name Mappings -->
<controls:SettingsGroup x:Uid="PowerDisplay_CustomVcpMappings_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander
x:Uid="PowerDisplay_CustomVcpMappings"
HeaderIcon="{ui:FontIcon Glyph=&#xE70F;}"
IsExpanded="{x:Bind ViewModel.HasCustomVcpMappings, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.CustomVcpMappings, Mode=OneWay}">
<tkcontrols:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="pdmodels:CustomVcpValueMapping">
<tkcontrols:SettingsCard Description="{x:Bind VcpCodeDisplayName}" Header="{x:Bind DisplaySummary}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
Click="EditCustomMapping_Click"
Content="{ui:FontIcon Glyph=&#xE70F;,
FontSize=14}"
Style="{StaticResource SubtleButtonStyle}"
Tag="{x:Bind}"
ToolTipService.ToolTip="Edit" />
<Button
Click="DeleteCustomMapping_Click"
Content="{ui:FontIcon Glyph=&#xE74D;,
FontSize=14}"
Style="{StaticResource SubtleButtonStyle}"
Tag="{x:Bind}"
ToolTipService.ToolTip="Delete" />
</StackPanel>
</tkcontrols:SettingsCard>
</DataTemplate>
</tkcontrols:SettingsExpander.ItemTemplate>
<!-- Add mapping button -->
<Button x:Uid="PowerDisplay_AddCustomMappingButton" Click="AddCustomMapping_Click">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon FontSize="14" Glyph="&#xE710;" />
<TextBlock x:Uid="PowerDisplay_AddCustomMapping_Text" />
</StackPanel>
</Button>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_Profiles_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander
x:Uid="PowerDisplay_QuickProfiles"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}"
IsExpanded="True"
IsExpanded="{x:Bind ViewModel.HasProfiles, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.Profiles, Mode=OneWay}">
<tkcontrols:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="pdmodels:PowerDisplayProfile">

View File

@@ -133,6 +133,65 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return ProfileHelper.GenerateUniqueProfileName(existingNames, baseName);
}
// Custom VCP Mapping event handlers
private async void AddCustomMapping_Click(object sender, RoutedEventArgs e)
{
var dialog = new CustomVcpMappingEditorDialog(ViewModel.Monitors);
dialog.XamlRoot = this.XamlRoot;
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary && dialog.ResultMapping != null)
{
ViewModel.AddCustomVcpMapping(dialog.ResultMapping);
}
}
private async void EditCustomMapping_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button button || button.Tag is not CustomVcpValueMapping mapping)
{
return;
}
var dialog = new CustomVcpMappingEditorDialog(ViewModel.Monitors);
dialog.XamlRoot = this.XamlRoot;
dialog.PreFillMapping(mapping);
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary && dialog.ResultMapping != null)
{
ViewModel.UpdateCustomVcpMapping(mapping, dialog.ResultMapping);
}
}
private async void DeleteCustomMapping_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button button || button.Tag is not CustomVcpValueMapping mapping)
{
return;
}
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
var dialog = new ContentDialog
{
XamlRoot = this.XamlRoot,
Title = resourceLoader.GetString("PowerDisplay_CustomMapping_Delete_Title"),
Content = resourceLoader.GetString("PowerDisplay_CustomMapping_Delete_Message"),
PrimaryButtonText = resourceLoader.GetString("Yes"),
CloseButtonText = resourceLoader.GetString("No"),
DefaultButton = ContentDialogButton.Close,
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
ViewModel.DeleteCustomVcpMapping(mapping);
}
}
// Flag to prevent reentrant handling during programmatic checkbox changes
private bool _isRestoringColorTempCheckbox;

View File

@@ -2726,6 +2726,9 @@ From there, simply click on one of the supported files in the File Explorer and
<data name="MouseUtils_CursorWrap_DisableWrapDuringDrag.Content" xml:space="preserve">
<value>Disable wrapping while dragging</value>
</data>
<data name="MouseUtils_CursorWrap_DisableOnSingleMonitor.Content" xml:space="preserve">
<value>Disable wrapping when using a single monitor</value>
</data>
<data name="MouseUtils_CursorWrap_AutoActivate.Header" xml:space="preserve">
<value>Auto-activate on startup</value>
</data>
@@ -5995,6 +5998,69 @@ The break timer font matches the text font.</value>
<data name="PowerDisplay_ShowIdentifyMonitorsButton.Description" xml:space="preserve">
<value>Show or hide the identify monitors button in the Power Display flyout</value>
</data>
<data name="PowerDisplay_CustomVcpMappings_GroupSettings.Header" xml:space="preserve">
<value>Custom VCP Name Mappings</value>
</data>
<data name="PowerDisplay_CustomVcpMappings.Header" xml:space="preserve">
<value>Custom name mappings</value>
</data>
<data name="PowerDisplay_CustomVcpMappings.Description" xml:space="preserve">
<value>Define custom display names for color temperature presets and input sources</value>
</data>
<data name="PowerDisplay_AddCustomMappingButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Add custom mapping</value>
</data>
<data name="PowerDisplay_AddCustomMapping_Text.Text" xml:space="preserve">
<value>Add mapping</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_Title" xml:space="preserve">
<value>Custom VCP Name Mapping</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_VcpCode.Header" xml:space="preserve">
<value>VCP Code</value>
</data>
<data name="PowerDisplay_VcpCode_Name_0x14" xml:space="preserve">
<value>Color Temperature</value>
</data>
<data name="PowerDisplay_VcpCode_Name_0x60" xml:space="preserve">
<value>Input Source</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomName.Header" xml:space="preserve">
<value>Custom Name</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomName.PlaceholderText" xml:space="preserve">
<value>Enter custom name</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_ValueComboBox.Header" xml:space="preserve">
<value>Value</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomValueOption" xml:space="preserve">
<value>Custom value...</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomValueInput.Header" xml:space="preserve">
<value>Enter custom value (hex)</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomValueInput.PlaceholderText" xml:space="preserve">
<value>e.g., 0x11 or 17</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_ApplyToAll.Header" xml:space="preserve">
<value>Apply to all monitors</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_ApplyToAll.OnContent" xml:space="preserve">
<value>On</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_ApplyToAll.OffContent" xml:space="preserve">
<value>Off</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_SelectMonitor.Header" xml:space="preserve">
<value>Select monitor</value>
</data>
<data name="PowerDisplay_CustomMapping_Delete_Title" xml:space="preserve">
<value>Delete custom mapping?</value>
</data>
<data name="PowerDisplay_CustomMapping_Delete_Message" xml:space="preserve">
<value>This custom name mapping will be permanently removed.</value>
</data>
<data name="Hosts_Backup_GroupSettings.Header" xml:space="preserve">
<value>Backup</value>
</data>

View File

@@ -116,6 +116,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Null-safe access in case property wasn't upgraded yet - default to 0 (Both)
_cursorWrapWrapMode = CursorWrapSettingsConfig.Properties.WrapMode?.Value ?? 0;
// Null-safe access in case property wasn't upgraded yet - default to false
_cursorWrapDisableOnSingleMonitor = CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor?.Value ?? false;
int isEnabled = 0;
Utilities.NativeMethods.SystemParametersInfo(Utilities.NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0);
@@ -1003,13 +1006,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GeneralSettingsConfig.Enabled.CursorWrap = value;
OnPropertyChanged(nameof(IsCursorWrapEnabled));
// Auto-enable the AutoActivate setting when CursorWrap is enabled
// This ensures cursor wrapping is active immediately after enabling
if (value && !_cursorWrapAutoActivate)
{
CursorWrapAutoActivate = true;
}
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
@@ -1114,6 +1110,34 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool CursorWrapDisableOnSingleMonitor
{
get
{
return _cursorWrapDisableOnSingleMonitor;
}
set
{
if (value != _cursorWrapDisableOnSingleMonitor)
{
_cursorWrapDisableOnSingleMonitor = value;
// Ensure the property exists before setting value
if (CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor == null)
{
CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor = new BoolProperty(value);
}
else
{
CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor.Value = value;
}
NotifyCursorWrapPropertyChanged();
}
}
}
public void NotifyCursorWrapPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);
@@ -1186,5 +1210,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _cursorWrapAutoActivate;
private bool _cursorWrapDisableWrapDuringDrag; // Will be initialized in constructor from settings
private int _cursorWrapWrapMode; // 0=Both, 1=VerticalOnly, 2=HorizontalOnly
private bool _cursorWrapDisableOnSingleMonitor; // Disable cursor wrap when only one monitor is connected
}
}

View File

@@ -36,6 +36,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public PowerDisplayViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<PowerDisplaySettings> powerDisplaySettingsRepository, Func<string, int> ipcMSGCallBackFunc)
{
// Set up localized VCP code names for UI display
VcpNames.LocalizedCodeNameProvider = GetLocalizedVcpCodeName;
// To obtain the general settings configurations of PowerToys Settings.
ArgumentNullException.ThrowIfNull(settingsRepository);
@@ -56,9 +59,15 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
// Subscribe to collection changes for HasProfiles binding
_profiles.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasProfiles));
// Load profiles
LoadProfiles();
// Load custom VCP mappings
LoadCustomVcpMappings();
// Listen for monitor refresh events from PowerDisplay.exe
NativeEventWaiter.WaitForEventLoop(
Constants.RefreshPowerDisplayMonitorsEvent(),
@@ -446,21 +455,28 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Profile-related fields
private ObservableCollection<PowerDisplayProfile> _profiles = new ObservableCollection<PowerDisplayProfile>();
// Custom VCP mapping fields
private ObservableCollection<CustomVcpValueMapping> _customVcpMappings;
/// <summary>
/// Gets or sets collection of available profiles (for button display)
/// Gets collection of custom VCP value name mappings
/// </summary>
public ObservableCollection<PowerDisplayProfile> Profiles
{
get => _profiles;
set
{
if (_profiles != value)
{
_profiles = value;
OnPropertyChanged();
}
}
}
public ObservableCollection<CustomVcpValueMapping> CustomVcpMappings => _customVcpMappings;
/// <summary>
/// Gets whether there are any custom VCP mappings (for UI binding)
/// </summary>
public bool HasCustomVcpMappings => _customVcpMappings?.Count > 0;
/// <summary>
/// Gets collection of available profiles (for button display)
/// </summary>
public ObservableCollection<PowerDisplayProfile> Profiles => _profiles;
/// <summary>
/// Gets whether there are any profiles (for UI binding)
/// </summary>
public bool HasProfiles => _profiles?.Count > 0;
public void RefreshEnabledState()
{
@@ -646,6 +662,109 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
/// <summary>
/// Load custom VCP mappings from settings
/// </summary>
private void LoadCustomVcpMappings()
{
List<CustomVcpValueMapping> mappings;
try
{
mappings = _settings.Properties.CustomVcpMappings ?? new List<CustomVcpValueMapping>();
Logger.LogInfo($"Loaded {mappings.Count} custom VCP mappings");
}
catch (Exception ex)
{
Logger.LogError($"Failed to load custom VCP mappings: {ex.Message}");
mappings = new List<CustomVcpValueMapping>();
}
_customVcpMappings = new ObservableCollection<CustomVcpValueMapping>(mappings);
_customVcpMappings.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasCustomVcpMappings));
OnPropertyChanged(nameof(CustomVcpMappings));
OnPropertyChanged(nameof(HasCustomVcpMappings));
}
/// <summary>
/// Add a new custom VCP mapping.
/// No duplicate checking - mappings are resolved by order (first match wins in VcpNames).
/// </summary>
public void AddCustomVcpMapping(CustomVcpValueMapping mapping)
{
if (mapping == null)
{
return;
}
CustomVcpMappings.Add(mapping);
Logger.LogInfo($"Added custom VCP mapping: VCP=0x{mapping.VcpCode:X2}, Value=0x{mapping.Value:X2} -> {mapping.CustomName}");
SaveCustomVcpMappings();
}
/// <summary>
/// Update an existing custom VCP mapping
/// </summary>
public void UpdateCustomVcpMapping(CustomVcpValueMapping oldMapping, CustomVcpValueMapping newMapping)
{
if (oldMapping == null || newMapping == null)
{
return;
}
var index = CustomVcpMappings.IndexOf(oldMapping);
if (index >= 0)
{
CustomVcpMappings[index] = newMapping;
Logger.LogInfo($"Updated custom VCP mapping at index {index}");
SaveCustomVcpMappings();
}
}
/// <summary>
/// Delete a custom VCP mapping
/// </summary>
public void DeleteCustomVcpMapping(CustomVcpValueMapping mapping)
{
if (mapping == null)
{
return;
}
if (CustomVcpMappings.Remove(mapping))
{
Logger.LogInfo($"Deleted custom VCP mapping: VCP=0x{mapping.VcpCode:X2}, Value=0x{mapping.Value:X2}");
SaveCustomVcpMappings();
}
}
/// <summary>
/// Save custom VCP mappings to settings
/// </summary>
private void SaveCustomVcpMappings()
{
_settings.Properties.CustomVcpMappings = CustomVcpMappings.ToList();
NotifySettingsChanged();
// Signal PowerDisplay to reload settings
SignalSettingsUpdated();
}
/// <summary>
/// Provides localized VCP code names for UI display.
/// Looks for resource string with pattern "PowerDisplay_VcpCode_Name_0xXX".
/// Returns null for unknown codes to use the default MCCS name.
/// </summary>
#nullable enable
private static string? GetLocalizedVcpCodeName(byte vcpCode)
{
var resourceKey = $"PowerDisplay_VcpCode_Name_0x{vcpCode:X2}";
var localizedName = ResourceLoaderInstance.ResourceLoader.GetString(resourceKey);
// ResourceLoader returns empty string if key not found
return string.IsNullOrEmpty(localizedName) ? null : localizedName;
}
#nullable restore
private void NotifySettingsChanged()
{
// Skip during initialization when SendConfigMSG is not yet set