Compare commits

...

14 Commits

Author SHA1 Message Date
Mike Griese
89c3229658 fine spellbot 2026-02-05 11:38:18 -06:00
Mike Griese
78378e1d0a CmdPal: Add Dock API
This doesn't actually add the dock. It just adds the API for it.

Extension authors can use this to create their own dock bands.

re: #45201
2026-02-05 10:26:53 -06:00
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
Mario Hewardt
8d9de117b9 Adds a video trim dialog to ZoomIt (#45334)
## Summary of the Pull Request
Adds a video trim dialog to ZoomIt

## PR Checklist
Closes 45333

## Validation Steps Performed
Manual validation

---------

Co-authored-by: Mark Russinovich <markruss@ntdev.microsoft.com>
Co-authored-by: foxmsft <foxmsft@hotmail.com>
2026-02-03 13:05:31 -08:00
Jiří Polášek
42a7213644 CmdPal: Supress warning CsWinRT1028 for DeleteObjectSafeHandle (#45324)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR adds a local suppression for warning CsWinRT1028: Class should
be marked partial for source generated class `DeleteObjectSafeHandle`.

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

- [x] Related to: #42574
<!-- - [ ] 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-03 12:12:38 -06:00
64 changed files with 11302 additions and 509 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

@@ -206,6 +206,7 @@ Bilibili
BVID
capturevideosample
cmdow
Contoso
Controlz
cortana
devhints

View File

@@ -0,0 +1,63 @@
acq
APPLYTOSUBMENUS
AUDCLNT
bitmaps
BUFFERFLAGS
centiseconds
Ctl
CTLCOLOR
CTLCOLORBTN
CTLCOLORDLG
CTLCOLOREDIT
CTLCOLORLISTBOX
CTrim
DFCS
dlg
dlu
DONTCARE
DRAWITEM
DRAWITEMSTRUCT
DWLP
EDITCONTROL
ENABLEHOOK
FDE
GETCHANNELRECT
GETCHECK
GETTHUMBRECT
GIFs
HTBOTTOMRIGHT
HTHEME
KSDATAFORMAT
LEFTNOWORDWRAP
letterbox
lld
logfont
lround
MENUINFO
mic
MMRESULT
OWNERDRAW
PBGRA
pfdc
playhead
pwfx
quantums
REFKNOWNFOLDERID
reposted
SCROLLSIZEGRIP
SETDEFID
SETRECT
SHAREMODE
SHAREVIOLATION
STREAMFLAGS
submix
tci
TEXTMETRIC
tme
TRACKMOUSEEVENT
Unadvise
WASAPI
WAVEFORMATEX
WAVEFORMATEXTENSIBLE
wil
WMU

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

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

@@ -1,9 +1,24 @@
#include "pch.h"
#include "AudioSampleGenerator.h"
#include "CaptureFrameWait.h"
#include "LoopbackCapture.h"
#include <wrl/client.h>
extern TCHAR g_MicrophoneDeviceId[];
namespace
{
// Declare the IMemoryBufferByteAccess interface for accessing raw buffer data
MIDL_INTERFACE("5b0d3235-4dba-4d44-8657-1f1d0f83e9a3")
IMemoryBufferByteAccess : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetBuffer(
BYTE** value,
UINT32* capacity) = 0;
};
}
namespace winrt
{
using namespace Windows::Foundation;
@@ -19,17 +34,23 @@ namespace winrt
using namespace Windows::Devices::Enumeration;
}
AudioSampleGenerator::AudioSampleGenerator()
AudioSampleGenerator::AudioSampleGenerator(bool captureMicrophone, bool captureSystemAudio)
: m_captureMicrophone(captureMicrophone)
, m_captureSystemAudio(captureSystemAudio)
{
OutputDebugStringA(("AudioSampleGenerator created, captureMicrophone=" +
std::string(captureMicrophone ? "true" : "false") +
", captureSystemAudio=" + std::string(captureSystemAudio ? "true" : "false") + "\n").c_str());
m_audioEvent.create(wil::EventOptions::ManualReset);
m_endEvent.create(wil::EventOptions::ManualReset);
m_startEvent.create(wil::EventOptions::ManualReset);
m_asyncInitialized.create(wil::EventOptions::ManualReset);
}
AudioSampleGenerator::~AudioSampleGenerator()
{
Stop();
if (m_started.load())
if (m_audioGraph)
{
m_audioGraph.Close();
}
@@ -40,6 +61,10 @@ winrt::IAsyncAction AudioSampleGenerator::InitializeAsync()
auto expected = false;
if (m_initialized.compare_exchange_strong(expected, true))
{
// Reset state in case this instance is reused.
m_endEvent.ResetEvent();
m_startEvent.ResetEvent();
// Initialize the audio graph
auto audioGraphSettings = winrt::AudioGraphSettings(winrt::AudioRenderCategory::Media);
auto audioGraphResult = co_await winrt::AudioGraph::CreateAsync(audioGraphSettings);
@@ -49,28 +74,88 @@ winrt::IAsyncAction AudioSampleGenerator::InitializeAsync()
}
m_audioGraph = audioGraphResult.Graph();
// Initialize the selected microphone
auto defaultMicrophoneId = winrt::MediaDevice::GetDefaultAudioCaptureId(winrt::AudioDeviceRole::Default);
auto microphoneId = (g_MicrophoneDeviceId[0] == 0) ? defaultMicrophoneId : winrt::to_hstring(g_MicrophoneDeviceId);
auto microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(microphoneId);
// Get AudioGraph encoding properties for resampling
auto graphProps = m_audioGraph.EncodingProperties();
m_graphSampleRate = graphProps.SampleRate();
m_graphChannels = graphProps.ChannelCount();
// Initialize audio input and output nodes
auto inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
if (inputNodeResult.Status() != winrt::AudioDeviceNodeCreationStatus::Success && microphoneId != defaultMicrophoneId)
{
// If the selected microphone failed, try again with the default
microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(defaultMicrophoneId);
inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
}
if (inputNodeResult.Status() != winrt::AudioDeviceNodeCreationStatus::Success)
{
throw winrt::hresult_error(E_FAIL, L"Failed to initialize input audio node!");
}
m_audioInputNode = inputNodeResult.DeviceInputNode();
OutputDebugStringA(("AudioGraph initialized: " + std::to_string(m_graphSampleRate) +
" Hz, " + std::to_string(m_graphChannels) + " ch\n").c_str());
// Create submix node to mix microphone and loopback audio
m_submixNode = m_audioGraph.CreateSubmixNode();
m_audioOutputNode = m_audioGraph.CreateFrameOutputNode();
m_submixNode.AddOutgoingConnection(m_audioOutputNode);
// Initialize WASAPI loopback capture for system audio (if enabled)
if (m_captureSystemAudio)
{
m_loopbackCapture = std::make_unique<LoopbackCapture>();
}
if (m_loopbackCapture && SUCCEEDED(m_loopbackCapture->Initialize()))
{
auto loopbackFormat = m_loopbackCapture->GetFormat();
if (loopbackFormat)
{
m_loopbackChannels = loopbackFormat->nChannels;
m_loopbackSampleRate = loopbackFormat->nSamplesPerSec;
m_resampleRatio = static_cast<double>(m_loopbackSampleRate) / static_cast<double>(m_graphSampleRate);
OutputDebugStringA(("Loopback initialized: " + std::to_string(m_loopbackSampleRate) +
" Hz, " + std::to_string(m_loopbackChannels) + " ch, resample ratio=" +
std::to_string(m_resampleRatio) + "\n").c_str());
}
}
else if (m_captureSystemAudio)
{
OutputDebugStringA("WARNING: Failed to initialize loopback capture\n");
m_loopbackCapture.reset();
}
// Always initialize a microphone input node to keep the AudioGraph running at real-time pace.
// When mic capture is disabled, we mute it so only loopback audio is captured.
{
auto defaultMicrophoneId = winrt::MediaDevice::GetDefaultAudioCaptureId(winrt::AudioDeviceRole::Default);
auto microphoneId = (m_captureMicrophone && g_MicrophoneDeviceId[0] != 0)
? winrt::to_hstring(g_MicrophoneDeviceId)
: defaultMicrophoneId;
if (!microphoneId.empty())
{
auto microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(microphoneId);
// Initialize audio input node
auto inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
if (inputNodeResult.Status() != winrt::AudioDeviceNodeCreationStatus::Success && microphoneId != defaultMicrophoneId)
{
// If the selected microphone failed, try again with the default
microphone = co_await winrt::DeviceInformation::CreateFromIdAsync(defaultMicrophoneId);
inputNodeResult = co_await m_audioGraph.CreateDeviceInputNodeAsync(winrt::MediaCategory::Media, m_audioGraph.EncodingProperties(), microphone);
}
if (inputNodeResult.Status() == winrt::AudioDeviceNodeCreationStatus::Success)
{
m_audioInputNode = inputNodeResult.DeviceInputNode();
m_audioInputNode.AddOutgoingConnection(m_submixNode);
// If mic capture is disabled, mute the input so only loopback is captured
if (!m_captureMicrophone)
{
m_audioInputNode.OutgoingGain(0.0);
OutputDebugStringA("Mic input created but muted (loopback-only mode)\n");
}
else
{
OutputDebugStringA("Mic input created and active\n");
}
}
}
}
// Loopback capture is only required when system audio capture is enabled
if (m_captureSystemAudio && !m_loopbackCapture)
{
throw winrt::hresult_error(E_FAIL, L"Failed to initialize loopback audio capture!");
}
// Hookup audio nodes
m_audioInputNode.AddOutgoingConnection(m_audioOutputNode);
m_audioGraph.QuantumStarted({ this, &AudioSampleGenerator::OnAudioQuantumStarted });
m_asyncInitialized.SetEvent();
@@ -86,7 +171,37 @@ winrt::AudioEncodingProperties AudioSampleGenerator::GetEncodingProperties()
std::optional<winrt::MediaStreamSample> AudioSampleGenerator::TryGetNextSample()
{
CheckInitialized();
CheckStarted();
// The MediaStreamSource can request audio samples before we've started the audio graph.
// Instead of throwing (which crashes the app), wait until either Start() is called
// or Stop() signals end-of-stream.
if (!m_started.load())
{
std::vector<HANDLE> events = { m_endEvent.get(), m_startEvent.get() };
auto waitResult = WaitForMultipleObjectsEx(static_cast<DWORD>(events.size()), events.data(), false, INFINITE, false);
auto eventIndex = -1;
switch (waitResult)
{
case WAIT_OBJECT_0:
case WAIT_OBJECT_0 + 1:
eventIndex = waitResult - WAIT_OBJECT_0;
break;
}
WINRT_VERIFY(eventIndex >= 0);
if (events[eventIndex] == m_endEvent.get())
{
// End event signaled, but check if there are any remaining samples in the queue
auto lock = m_lock.lock_exclusive();
if (!m_samples.empty())
{
std::optional result(m_samples.front());
m_samples.pop_front();
return result;
}
return std::nullopt;
}
}
{
auto lock = m_lock.lock_exclusive();
@@ -118,11 +233,25 @@ std::optional<winrt::MediaStreamSample> AudioSampleGenerator::TryGetNextSample()
auto signaledEvent = events[eventIndex];
if (signaledEvent == m_endEvent.get())
{
// End was signaled, but check for any remaining samples before returning nullopt
auto lock = m_lock.lock_exclusive();
if (!m_samples.empty())
{
std::optional result(m_samples.front());
m_samples.pop_front();
return result;
}
return std::nullopt;
}
else
{
auto lock = m_lock.lock_exclusive();
if (m_samples.empty())
{
// Spurious wake or race - no samples available
// If end is signaled, return nullopt
return m_endEvent.is_signaled() ? std::nullopt : std::optional<winrt::MediaStreamSample>{};
}
std::optional result(m_samples.front());
m_samples.pop_front();
return result;
@@ -135,23 +264,357 @@ void AudioSampleGenerator::Start()
auto expected = false;
if (m_started.compare_exchange_strong(expected, true))
{
m_endEvent.ResetEvent();
m_startEvent.SetEvent();
// Start loopback capture if available
if (m_loopbackCapture)
{
// Clear any stale samples
{
auto lock = m_loopbackBufferLock.lock_exclusive();
m_loopbackBuffer.clear();
}
m_resampleInputBuffer.clear();
m_resampleInputPos = 0.0;
m_loopbackCapture->Start();
}
m_audioGraph.Start();
}
}
void AudioSampleGenerator::Stop()
{
CheckInitialized();
if (m_started.load())
// Stop may be called during teardown even if initialization hasn't completed.
// It must never throw.
if (!m_initialized.load())
{
m_asyncInitialized.wait();
m_audioGraph.Stop();
m_endEvent.SetEvent();
return;
}
m_asyncInitialized.wait();
// Stop loopback capture first
if (m_loopbackCapture)
{
m_loopbackCapture->Stop();
}
// Flush any remaining samples from the loopback capture before stopping the audio graph
FlushRemainingAudio();
// 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);
// Combine all remaining queued samples into one final sample so it can be
// returned immediately without waiting for additional TryGetNextSample calls
CombineQueuedSamples();
// NOW signal end event - this allows TryGetNextSample to return remaining
// queued samples and then return nullopt
m_endEvent.SetEvent();
m_audioEvent.SetEvent(); // Also wake any waiting TryGetNextSample
// DO NOT clear m_loopbackBuffer or m_samples here - allow MediaTranscoder to
// consume remaining queued audio samples to avoid audio cutoff at end of recording.
// TryGetNextSample() will return nullopt once m_samples is empty and
// m_endEvent is signaled. Buffers will be cleaned up on destruction.
}
void AudioSampleGenerator::AppendResampledLoopbackSamples(std::vector<float> const& rawLoopbackSamples, bool flushRemaining)
{
if (rawLoopbackSamples.empty())
{
return;
}
m_resampleInputBuffer.insert(m_resampleInputBuffer.end(), rawLoopbackSamples.begin(), rawLoopbackSamples.end());
if (m_loopbackChannels == 0 || m_graphChannels == 0 || m_resampleRatio <= 0.0)
{
return;
}
std::vector<float> resampledSamples;
while (true)
{
const uint32_t inputFrames = static_cast<uint32_t>(m_resampleInputBuffer.size() / m_loopbackChannels);
if (inputFrames == 0)
{
break;
}
if (!flushRemaining)
{
if (inputFrames < 2 || (m_resampleInputPos + 1.0) >= inputFrames)
{
break;
}
}
else
{
if (m_resampleInputPos >= inputFrames)
{
break;
}
}
uint32_t inputFrame = static_cast<uint32_t>(m_resampleInputPos);
double frac = m_resampleInputPos - inputFrame;
uint32_t nextFrame = (inputFrame + 1 < inputFrames) ? (inputFrame + 1) : inputFrame;
for (uint32_t outCh = 0; outCh < m_graphChannels; outCh++)
{
float sample = 0.0f;
if (m_loopbackChannels == m_graphChannels)
{
uint32_t idx1 = inputFrame * m_loopbackChannels + outCh;
uint32_t idx2 = nextFrame * m_loopbackChannels + outCh;
float s1 = m_resampleInputBuffer[idx1];
float s2 = m_resampleInputBuffer[idx2];
sample = static_cast<float>(s1 * (1.0 - frac) + s2 * frac);
}
else if (m_loopbackChannels > m_graphChannels)
{
float sum = 0.0f;
for (uint32_t inCh = 0; inCh < m_loopbackChannels; inCh++)
{
uint32_t idx1 = inputFrame * m_loopbackChannels + inCh;
uint32_t idx2 = nextFrame * m_loopbackChannels + inCh;
float s1 = m_resampleInputBuffer[idx1];
float s2 = m_resampleInputBuffer[idx2];
sum += static_cast<float>(s1 * (1.0 - frac) + s2 * frac);
}
sample = sum / m_loopbackChannels;
}
else
{
uint32_t idx1 = inputFrame * m_loopbackChannels;
uint32_t idx2 = nextFrame * m_loopbackChannels;
float s1 = m_resampleInputBuffer[idx1];
float s2 = m_resampleInputBuffer[idx2];
sample = static_cast<float>(s1 * (1.0 - frac) + s2 * frac);
}
resampledSamples.push_back(sample);
}
m_resampleInputPos += m_resampleRatio;
}
uint32_t consumedFrames = static_cast<uint32_t>(m_resampleInputPos);
if (consumedFrames > 0)
{
size_t samplesToErase = static_cast<size_t>(consumedFrames) * m_loopbackChannels;
if (samplesToErase >= m_resampleInputBuffer.size())
{
m_resampleInputBuffer.clear();
m_resampleInputPos = 0.0;
}
else
{
m_resampleInputBuffer.erase(m_resampleInputBuffer.begin(), m_resampleInputBuffer.begin() + samplesToErase);
m_resampleInputPos -= consumedFrames;
}
}
if (flushRemaining)
{
m_resampleInputBuffer.clear();
m_resampleInputPos = 0.0;
}
if (!resampledSamples.empty())
{
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
const size_t maxBufferSize = static_cast<size_t>(m_graphSampleRate) * m_graphChannels;
if (m_loopbackBuffer.size() + resampledSamples.size() > maxBufferSize)
{
size_t overflow = (m_loopbackBuffer.size() + resampledSamples.size()) - maxBufferSize;
if (overflow >= m_loopbackBuffer.size())
{
m_loopbackBuffer.clear();
}
else
{
m_loopbackBuffer.erase(m_loopbackBuffer.begin(), m_loopbackBuffer.begin() + overflow);
}
}
m_loopbackBuffer.insert(m_loopbackBuffer.end(), resampledSamples.begin(), resampledSamples.end());
}
}
void AudioSampleGenerator::FlushRemainingAudio()
{
// Called during stop to drain any remaining samples from loopback capture
// and convert them to MediaStreamSamples before the audio graph stops.
if (!m_loopbackCapture)
{
return;
}
auto lock = m_lock.lock_exclusive();
// Drain all remaining samples from the loopback capture client
std::vector<float> rawLoopbackSamples;
{
std::vector<float> tempSamples;
while (m_loopbackCapture->TryGetSamples(tempSamples))
{
rawLoopbackSamples.insert(rawLoopbackSamples.end(), tempSamples.begin(), tempSamples.end());
}
}
// Resample and channel-convert the loopback audio to match AudioGraph format
if (!rawLoopbackSamples.empty())
{
AppendResampledLoopbackSamples(rawLoopbackSamples, true);
}
// Now convert everything in m_loopbackBuffer to MediaStreamSamples
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
if (!m_loopbackBuffer.empty())
{
uint32_t outputSampleCount = static_cast<uint32_t>(m_loopbackBuffer.size());
std::vector<uint8_t> outputData(outputSampleCount * sizeof(float), 0);
float* outputFloats = reinterpret_cast<float*>(outputData.data());
for (uint32_t i = 0; i < outputSampleCount; i++)
{
float sample = m_loopbackBuffer[i];
if (sample > 1.0f) sample = 1.0f;
else if (sample < -1.0f) sample = -1.0f;
outputFloats[i] = sample;
}
m_loopbackBuffer.clear();
// Create buffer and sample
winrt::Buffer sampleBuffer(outputSampleCount * sizeof(float));
memcpy(sampleBuffer.data(), outputData.data(), outputData.size());
sampleBuffer.Length(static_cast<uint32_t>(outputData.size()));
if (sampleBuffer.Length() > 0)
{
const uint32_t sampleCount = sampleBuffer.Length() / sizeof(float);
const uint32_t frames = (m_graphChannels > 0) ? (sampleCount / m_graphChannels) : 0;
const int64_t durationTicks = (m_graphSampleRate > 0) ? (static_cast<int64_t>(frames) * 10000000LL / m_graphSampleRate) : 0;
const winrt::TimeSpan duration{ durationTicks };
winrt::TimeSpan timestamp{ 0 };
if (m_hasLastSampleTimestamp)
{
timestamp = winrt::TimeSpan{ m_lastSampleTimestamp.count() + m_lastSampleDuration.count() };
}
auto sample = winrt::MediaStreamSample::CreateFromBuffer(sampleBuffer, timestamp);
m_samples.push_back(sample);
m_audioEvent.SetEvent();
m_lastSampleTimestamp = timestamp;
m_lastSampleDuration = duration;
m_hasLastSampleTimestamp = true;
}
}
}
void AudioSampleGenerator::CombineQueuedSamples()
{
// Combine all queued samples into a single sample so it can be returned
// immediately in the next TryGetNextSample call. This is critical because
// once video ends, the MediaTranscoder may only request one more audio sample.
auto lock = m_lock.lock_exclusive();
if (m_samples.size() <= 1)
{
return;
}
// Calculate total size and collect all sample data
size_t totalBytes = 0;
std::vector<std::pair<winrt::Windows::Storage::Streams::IBuffer, winrt::Windows::Foundation::TimeSpan>> buffers;
winrt::Windows::Foundation::TimeSpan firstTimestamp{ 0 };
bool hasFirstTimestamp = false;
for (auto& sample : m_samples)
{
auto buffer = sample.Buffer();
if (buffer)
{
totalBytes += buffer.Length();
if (!hasFirstTimestamp)
{
firstTimestamp = sample.Timestamp();
hasFirstTimestamp = true;
}
buffers.push_back({ buffer, sample.Timestamp() });
}
}
if (totalBytes == 0)
{
return;
}
// Create combined buffer
winrt::Buffer combinedBuffer(static_cast<uint32_t>(totalBytes));
uint8_t* dest = combinedBuffer.data();
uint32_t offset = 0;
for (auto& [buffer, ts] : buffers)
{
uint32_t len = buffer.Length();
memcpy(dest + offset, buffer.data(), len);
offset += len;
}
combinedBuffer.Length(static_cast<uint32_t>(totalBytes));
// Create combined sample with first timestamp
auto combinedSample = winrt::Windows::Media::Core::MediaStreamSample::CreateFromBuffer(combinedBuffer, firstTimestamp);
// Clear queue and add combined sample
m_samples.clear();
m_samples.push_back(combinedSample);
// Update timestamp tracking
const uint32_t sampleCount = static_cast<uint32_t>(totalBytes) / sizeof(float);
const uint32_t frames = (m_graphChannels > 0) ? (sampleCount / m_graphChannels) : 0;
const int64_t durationTicks = (m_graphSampleRate > 0) ? (static_cast<int64_t>(frames) * 10000000LL / m_graphSampleRate) : 0;
m_lastSampleTimestamp = firstTimestamp;
m_lastSampleDuration = winrt::Windows::Foundation::TimeSpan{ durationTicks };
m_hasLastSampleTimestamp = true;
}
void AudioSampleGenerator::OnAudioQuantumStarted(winrt::AudioGraph const& sender, winrt::IInspectable const& args)
{
// Don't process if we're not actively recording
if (!m_started.load())
{
return;
}
{
auto lock = m_lock.lock_exclusive();
@@ -159,10 +622,101 @@ void AudioSampleGenerator::OnAudioQuantumStarted(winrt::AudioGraph const& sender
std::optional<winrt::TimeSpan> timestamp = frame.RelativeTime();
auto audioBuffer = frame.LockBuffer(winrt::AudioBufferAccessMode::Read);
// Get mic audio as a buffer (may be empty if no microphone)
auto sampleBuffer = winrt::Buffer::CreateCopyFromMemoryBuffer(audioBuffer);
sampleBuffer.Length(audioBuffer.Length());
auto sample = winrt::MediaStreamSample::CreateFromBuffer(sampleBuffer, timestamp.value());
m_samples.push_back(sample);
// Calculate expected samples per quantum (~10ms at graph sample rate)
// AudioGraph uses 10ms quantums by default
uint32_t expectedSamplesPerQuantum = (m_graphSampleRate / 100) * m_graphChannels;
uint32_t numMicSamples = audioBuffer.Length() / sizeof(float);
// Drain loopback samples regardless of whether we have mic audio
if (m_loopbackCapture)
{
std::vector<float> rawLoopbackSamples;
{
std::vector<float> tempSamples;
while (m_loopbackCapture->TryGetSamples(tempSamples))
{
rawLoopbackSamples.insert(rawLoopbackSamples.end(), tempSamples.begin(), tempSamples.end());
}
}
// Resample and channel-convert the loopback audio to match AudioGraph format
if (!rawLoopbackSamples.empty())
{
AppendResampledLoopbackSamples(rawLoopbackSamples);
}
}
// Determine the actual number of samples we'll output
// Use mic sample count if mic is enabled
uint32_t outputSampleCount = m_captureMicrophone ? numMicSamples : expectedSamplesPerQuantum;
// If microphone is disabled, create a buffer with only loopback audio
if (!m_captureMicrophone && outputSampleCount > 0)
{
// Create a buffer filled with loopback audio or silence
std::vector<uint8_t> outputData(outputSampleCount * sizeof(float), 0);
float* outputFloats = reinterpret_cast<float*>(outputData.data());
{
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
uint32_t samplesToUse = min(outputSampleCount, static_cast<uint32_t>(m_loopbackBuffer.size()));
for (uint32_t i = 0; i < samplesToUse; i++)
{
float sample = m_loopbackBuffer[i];
if (sample > 1.0f) sample = 1.0f;
else if (sample < -1.0f) sample = -1.0f;
outputFloats[i] = sample;
}
if (samplesToUse > 0)
{
m_loopbackBuffer.erase(m_loopbackBuffer.begin(), m_loopbackBuffer.begin() + samplesToUse);
}
}
// Create a new buffer with our loopback data
sampleBuffer = winrt::Buffer(outputSampleCount * sizeof(float));
memcpy(sampleBuffer.data(), outputData.data(), outputData.size());
sampleBuffer.Length(static_cast<uint32_t>(outputData.size()));
}
else if (m_captureMicrophone && numMicSamples > 0)
{
// Mix loopback into mic samples
auto loopbackLock = m_loopbackBufferLock.lock_exclusive();
float* bufferData = reinterpret_cast<float*>(sampleBuffer.data());
uint32_t samplesToMix = min(numMicSamples, static_cast<uint32_t>(m_loopbackBuffer.size()));
for (uint32_t i = 0; i < samplesToMix; i++)
{
float mixed = bufferData[i] + m_loopbackBuffer[i];
if (mixed > 1.0f) mixed = 1.0f;
else if (mixed < -1.0f) mixed = -1.0f;
bufferData[i] = mixed;
}
if (samplesToMix > 0)
{
m_loopbackBuffer.erase(m_loopbackBuffer.begin(), m_loopbackBuffer.begin() + samplesToMix);
}
}
if (sampleBuffer.Length() > 0)
{
auto sample = winrt::MediaStreamSample::CreateFromBuffer(sampleBuffer, timestamp.value());
m_samples.push_back(sample);
const uint32_t sampleCount = sampleBuffer.Length() / sizeof(float);
const uint32_t frames = (m_graphChannels > 0) ? (sampleCount / m_graphChannels) : 0;
const int64_t durationTicks = (m_graphSampleRate > 0) ? (static_cast<int64_t>(frames) * 10000000LL / m_graphSampleRate) : 0;
m_lastSampleTimestamp = timestamp.value();
m_lastSampleDuration = winrt::TimeSpan{ durationTicks };
m_hasLastSampleTimestamp = true;
}
}
m_audioEvent.SetEvent();
}

View File

@@ -1,9 +1,11 @@
#pragma once
#include "LoopbackCapture.h"
class AudioSampleGenerator
{
public:
AudioSampleGenerator();
AudioSampleGenerator(bool captureMicrophone = true, bool captureSystemAudio = true);
~AudioSampleGenerator();
winrt::Windows::Foundation::IAsyncAction InitializeAsync();
@@ -18,6 +20,10 @@ private:
winrt::Windows::Media::Audio::AudioGraph const& sender,
winrt::Windows::Foundation::IInspectable const& args);
void FlushRemainingAudio();
void CombineQueuedSamples();
void AppendResampledLoopbackSamples(std::vector<float> const& rawLoopbackSamples, bool flushRemaining = false);
void CheckInitialized()
{
if (!m_initialized.load())
@@ -37,12 +43,31 @@ private:
private:
winrt::Windows::Media::Audio::AudioGraph m_audioGraph{ nullptr };
winrt::Windows::Media::Audio::AudioDeviceInputNode m_audioInputNode{ nullptr };
winrt::Windows::Media::Audio::AudioSubmixNode m_submixNode{ nullptr };
winrt::Windows::Media::Audio::AudioFrameOutputNode m_audioOutputNode{ nullptr };
std::unique_ptr<LoopbackCapture> m_loopbackCapture;
std::vector<float> m_loopbackBuffer; // Accumulated loopback samples (resampled to match AudioGraph)
wil::srwlock m_loopbackBufferLock;
uint32_t m_loopbackChannels = 2;
uint32_t m_loopbackSampleRate = 48000;
uint32_t m_graphSampleRate = 48000;
uint32_t m_graphChannels = 2;
double m_resampleRatio = 1.0; // loopbackSampleRate / graphSampleRate
winrt::Windows::Foundation::TimeSpan m_lastSampleTimestamp{};
winrt::Windows::Foundation::TimeSpan m_lastSampleDuration{};
bool m_hasLastSampleTimestamp = false;
std::vector<float> m_resampleInputBuffer; // raw loopback samples buffered for resampling
double m_resampleInputPos = 0.0; // fractional input frame position for resampling
wil::srwlock m_lock;
wil::unique_event m_audioEvent;
wil::unique_event m_endEvent;
wil::unique_event m_startEvent;
wil::unique_event m_asyncInitialized;
std::deque<winrt::Windows::Media::Core::MediaStreamSample> m_samples;
std::atomic<bool> m_initialized = false;
std::atomic<bool> m_started = false;
bool m_captureMicrophone = true;
bool m_captureSystemAudio = true;
};

View File

@@ -846,7 +846,6 @@ LRESULT CALLBACK DemoTypeHookProc( int nCode, WPARAM wParam, LPARAM lParam )
if( g_UserDriven )
{
// Set baseline indentation to a blocking flag
// Otherwise indentation seeking will trigger user-driven injection events
g_BaselineIndentation = INDENT_SEEK_FLAG;
// Initialize the injection handler

View File

@@ -242,6 +242,13 @@ std::shared_ptr<GifRecordingSession> GifRecordingSession::Create(
//----------------------------------------------------------------------------
HRESULT GifRecordingSession::EncodeFrame(ID3D11Texture2D* frameTexture)
{
std::lock_guard<std::mutex> lock(m_encoderMutex);
if (m_encoderReleased)
{
OutputDebugStringW(L"EncodeFrame called after encoder released.\n");
return E_FAIL;
}
try
{
// Create a staging texture for CPU access
@@ -367,6 +374,7 @@ HRESULT GifRecordingSession::EncodeFrame(ID3D11Texture2D* frameTexture)
// Increment and log frame count
m_frameCount++;
m_hasAnyFrame.store(true);
OutputDebugStringW((L"GIF Frame #" + std::to_wstring(m_frameCount) + L" fully encoded and committed\n").c_str());
return S_OK;
@@ -405,6 +413,12 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
{
captureAttempts++;
auto frame = m_frameWait->TryGetNextFrame();
if (!frame && !m_isRecording)
{
// Recording was stopped while waiting for frame
OutputDebugStringW(L"[GIF] Recording stopped during frame wait\n");
break;
}
winrt::com_ptr<ID3D11Texture2D> croppedTexture;
@@ -472,8 +486,17 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
// Wait for the next frame interval
co_await winrt::resume_after(std::chrono::milliseconds(1000 / m_frameRate));
// Check again after resuming from sleep
if (!m_isRecording || m_closed)
{
OutputDebugStringW(L"[GIF] Loop exiting after resume_after\n");
break;
}
}
OutputDebugStringW(L"[GIF] Capture loop exited\n");
// Commit the GIF encoder
if (m_gifEncoder)
{
@@ -511,6 +534,10 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
CloseInternal();
}
}
// Ensure encoder resources are released in case caller forgets to Close explicitly.
ReleaseEncoderResources();
OutputDebugStringW(L"[GIF] StartAsync completing, about to co_return\n");
co_return;
}
@@ -521,18 +548,18 @@ winrt::IAsyncAction GifRecordingSession::StartAsync()
//----------------------------------------------------------------------------
void GifRecordingSession::Close()
{
OutputDebugStringW(L"[GIF] Close() called\n");
auto expected = false;
if (m_closed.compare_exchange_strong(expected, true))
{
expected = true;
if (!m_isRecording.compare_exchange_strong(expected, false))
{
CloseInternal();
}
else
{
m_frameWait->StopCapture();
}
OutputDebugStringW(L"[GIF] Setting m_closed = true\n");
// Signal the capture loop to stop
m_isRecording = false;
OutputDebugStringW(L"[GIF] Setting m_isRecording = false\n");
// Stop the frame wait to unblock any pending frame acquisition
m_frameWait->StopCapture();
OutputDebugStringW(L"[GIF] StopCapture called\n");
}
}
@@ -543,6 +570,42 @@ void GifRecordingSession::Close()
//----------------------------------------------------------------------------
void GifRecordingSession::CloseInternal()
{
ReleaseEncoderResources();
m_frameWait->StopCapture();
m_itemClosed.revoke();
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::ReleaseEncoderResources
// Ensures encoder/stream COM objects release the temp file handle so trim can reopen it.
//
//----------------------------------------------------------------------------
void GifRecordingSession::ReleaseEncoderResources()
{
std::lock_guard<std::mutex> lock(m_encoderMutex);
if (m_encoderReleased)
{
return;
}
// Commit only if we still own the encoder and it has not been committed; swallow failures.
if (m_gifEncoder)
{
try
{
m_gifEncoder->Commit();
}
catch (...)
{
}
}
m_encoderMetadataWriter = nullptr;
m_gifEncoder = nullptr;
m_wicStream = nullptr;
m_wicFactory = nullptr;
m_stream = nullptr;
m_encoderReleased = true;
}

View File

@@ -11,6 +11,7 @@
#include "CaptureFrameWait.h"
#include <d3d11_4.h>
#include <vector>
#include <mutex>
class GifRecordingSession : public std::enable_shared_from_this<GifRecordingSession>
{
@@ -27,6 +28,8 @@ public:
void EnableCursorCapture(bool enable = true) { m_frameWait->EnableCursorCapture(enable); }
void Close();
bool HasCapturedFrames() const { return m_hasAnyFrame.load(); }
private:
GifRecordingSession(
winrt::Direct3D11::IDirect3DDevice const& device,
@@ -35,6 +38,7 @@ private:
uint32_t frameRate,
winrt::Streams::IRandomAccessStream const& stream);
void CloseInternal();
void ReleaseEncoderResources();
HRESULT EncodeFrame(ID3D11Texture2D* texture);
private:
@@ -58,6 +62,9 @@ private:
std::atomic<bool> m_isRecording = false;
std::atomic<bool> m_closed = false;
std::atomic<bool> m_encoderReleased = false;
std::atomic<bool> m_hasAnyFrame = false;
std::mutex m_encoderMutex;
uint32_t m_frameWidth=0;
uint32_t m_frameHeight=0;

View File

@@ -0,0 +1,337 @@
#include "pch.h"
#include "LoopbackCapture.h"
#include <functiondiscoverykeys_devpkey.h>
#pragma comment(lib, "ole32.lib")
LoopbackCapture::LoopbackCapture()
{
m_stopEvent.create(wil::EventOptions::ManualReset);
m_samplesReadyEvent.create(wil::EventOptions::ManualReset);
}
LoopbackCapture::~LoopbackCapture()
{
Stop();
if (m_pwfx)
{
CoTaskMemFree(m_pwfx);
m_pwfx = nullptr;
}
}
HRESULT LoopbackCapture::Initialize()
{
if (m_initialized.load())
{
return S_OK;
}
HRESULT hr = CoCreateInstance(
__uuidof(MMDeviceEnumerator),
nullptr,
CLSCTX_ALL,
__uuidof(IMMDeviceEnumerator),
m_deviceEnumerator.put_void());
if (FAILED(hr))
{
return hr;
}
// Get the default audio render device (speakers/headphones)
hr = m_deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, m_device.put());
if (FAILED(hr))
{
return hr;
}
hr = m_device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, m_audioClient.put_void());
if (FAILED(hr))
{
return hr;
}
// Get the mix format
hr = m_audioClient->GetMixFormat(&m_pwfx);
if (FAILED(hr))
{
return hr;
}
// Initialize audio client in loopback mode
// AUDCLNT_STREAMFLAGS_LOOPBACK enables capturing what's being played on the device
hr = m_audioClient->Initialize(
AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_LOOPBACK,
1000000, // 100ms buffer to reduce capture latency
0,
m_pwfx,
nullptr);
if (FAILED(hr))
{
return hr;
}
hr = m_audioClient->GetService(__uuidof(IAudioCaptureClient), m_captureClient.put_void());
if (FAILED(hr))
{
return hr;
}
m_initialized.store(true);
return S_OK;
}
HRESULT LoopbackCapture::Start()
{
if (!m_initialized.load())
{
return E_NOT_VALID_STATE;
}
if (m_started.load())
{
return S_OK;
}
m_stopEvent.ResetEvent();
HRESULT hr = m_audioClient->Start();
if (FAILED(hr))
{
return hr;
}
m_started.store(true);
// Start capture thread
m_captureThread = std::thread(&LoopbackCapture::CaptureThread, this);
return S_OK;
}
void LoopbackCapture::Stop()
{
if (!m_started.load())
{
return;
}
m_stopEvent.SetEvent();
if (m_captureThread.joinable())
{
m_captureThread.join();
}
DrainCaptureClient();
if (m_audioClient)
{
m_audioClient->Stop();
}
m_started.store(false);
}
void LoopbackCapture::DrainCaptureClient()
{
if (!m_captureClient)
{
return;
}
while (true)
{
UINT32 packetLength = 0;
HRESULT hr = m_captureClient->GetNextPacketSize(&packetLength);
if (FAILED(hr) || packetLength == 0)
{
break;
}
BYTE* pData = nullptr;
UINT32 numFramesAvailable = 0;
DWORD flags = 0;
hr = m_captureClient->GetBuffer(&pData, &numFramesAvailable, &flags, nullptr, nullptr);
if (FAILED(hr))
{
break;
}
if (numFramesAvailable > 0)
{
std::vector<float> samples;
if (m_pwfx->wFormatTag == WAVE_FORMAT_IEEE_FLOAT ||
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
{
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
{
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
}
else
{
float* floatData = reinterpret_cast<float*>(pData);
samples.assign(floatData, floatData + (static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels));
}
}
else if (m_pwfx->wFormatTag == WAVE_FORMAT_PCM ||
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_PCM))
{
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
{
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
}
else if (m_pwfx->wBitsPerSample == 16)
{
int16_t* pcmData = reinterpret_cast<int16_t*>(pData);
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
for (size_t i = 0; i < samples.size(); i++)
{
samples[i] = static_cast<float>(pcmData[i]) / 32768.0f;
}
}
else if (m_pwfx->wBitsPerSample == 32)
{
int32_t* pcmData = reinterpret_cast<int32_t*>(pData);
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
for (size_t i = 0; i < samples.size(); i++)
{
samples[i] = static_cast<float>(pcmData[i]) / 2147483648.0f;
}
}
}
if (!samples.empty())
{
auto lock = m_lock.lock_exclusive();
m_sampleQueue.push_back(std::move(samples));
m_samplesReadyEvent.SetEvent();
}
}
hr = m_captureClient->ReleaseBuffer(numFramesAvailable);
if (FAILED(hr))
{
break;
}
}
}
void LoopbackCapture::CaptureThread()
{
while (WaitForSingleObject(m_stopEvent.get(), 10) == WAIT_TIMEOUT)
{
UINT32 packetLength = 0;
HRESULT hr = m_captureClient->GetNextPacketSize(&packetLength);
if (FAILED(hr))
{
break;
}
while (packetLength != 0)
{
BYTE* pData = nullptr;
UINT32 numFramesAvailable = 0;
DWORD flags = 0;
hr = m_captureClient->GetBuffer(&pData, &numFramesAvailable, &flags, nullptr, nullptr);
if (FAILED(hr))
{
break;
}
if (numFramesAvailable > 0)
{
std::vector<float> samples;
// Convert to float samples
if (m_pwfx->wFormatTag == WAVE_FORMAT_IEEE_FLOAT ||
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
{
// Already float format
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
{
// Insert silence
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
}
else
{
float* floatData = reinterpret_cast<float*>(pData);
samples.assign(floatData, floatData + (static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels));
}
}
else if (m_pwfx->wFormatTag == WAVE_FORMAT_PCM ||
(m_pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(m_pwfx)->SubFormat == KSDATAFORMAT_SUBTYPE_PCM))
{
// Convert PCM to float
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
{
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels, 0.0f);
}
else if (m_pwfx->wBitsPerSample == 16)
{
int16_t* pcmData = reinterpret_cast<int16_t*>(pData);
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
for (size_t i = 0; i < samples.size(); i++)
{
samples[i] = static_cast<float>(pcmData[i]) / 32768.0f;
}
}
else if (m_pwfx->wBitsPerSample == 32)
{
int32_t* pcmData = reinterpret_cast<int32_t*>(pData);
samples.resize(static_cast<size_t>(numFramesAvailable) * m_pwfx->nChannels);
for (size_t i = 0; i < samples.size(); i++)
{
samples[i] = static_cast<float>(pcmData[i]) / 2147483648.0f;
}
}
}
if (!samples.empty())
{
auto lock = m_lock.lock_exclusive();
m_sampleQueue.push_back(std::move(samples));
m_samplesReadyEvent.SetEvent();
}
}
hr = m_captureClient->ReleaseBuffer(numFramesAvailable);
if (FAILED(hr))
{
break;
}
hr = m_captureClient->GetNextPacketSize(&packetLength);
if (FAILED(hr))
{
break;
}
}
}
}
bool LoopbackCapture::TryGetSamples(std::vector<float>& samples)
{
auto lock = m_lock.lock_exclusive();
if (m_sampleQueue.empty())
{
return false;
}
samples = std::move(m_sampleQueue.front());
m_sampleQueue.pop_front();
if (m_sampleQueue.empty())
{
m_samplesReadyEvent.ResetEvent();
}
return true;
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <mmdeviceapi.h>
#include <audioclient.h>
#include <atomic>
#include <vector>
#include <deque>
#include <wil/resource.h>
class LoopbackCapture
{
public:
LoopbackCapture();
~LoopbackCapture();
HRESULT Initialize();
HRESULT Start();
void Stop();
// Returns audio samples in the format: PCM float, stereo, 48kHz
bool TryGetSamples(std::vector<float>& samples);
WAVEFORMATEX* GetFormat() const { return m_pwfx; }
uint32_t GetSampleRate() const { return m_pwfx ? m_pwfx->nSamplesPerSec : 48000; }
uint32_t GetChannels() const { return m_pwfx ? m_pwfx->nChannels : 2; }
private:
void CaptureThread();
void DrainCaptureClient();
winrt::com_ptr<IMMDeviceEnumerator> m_deviceEnumerator;
winrt::com_ptr<IMMDevice> m_device;
winrt::com_ptr<IAudioClient> m_audioClient;
winrt::com_ptr<IAudioCaptureClient> m_captureClient;
WAVEFORMATEX* m_pwfx{ nullptr };
wil::unique_event m_stopEvent;
wil::unique_event m_samplesReadyEvent;
std::thread m_captureThread;
wil::srwlock m_lock;
std::deque<std::vector<float>> m_sampleQueue;
std::atomic<bool> m_initialized{ false };
std::atomic<bool> m_started{ false };
};

View File

@@ -8,6 +8,579 @@
//==============================================================================
#include "pch.h"
#include "Utility.h"
#include <string>
#pragma comment(lib, "uxtheme.lib")
//----------------------------------------------------------------------------
// Dark Mode - Static/Global State
//----------------------------------------------------------------------------
static bool g_darkModeInitialized = false;
static bool g_darkModeEnabled = false;
static HBRUSH g_darkBackgroundBrush = nullptr;
static HBRUSH g_darkControlBrush = nullptr;
static HBRUSH g_darkSurfaceBrush = nullptr;
// Theme override from registry (defined in ZoomItSettings.h)
extern DWORD g_ThemeOverride;
// Preferred App Mode values for Windows 10/11 dark mode
enum class PreferredAppMode
{
Default,
AllowDark,
ForceDark,
ForceLight,
Max
};
// Undocumented ordinals from uxtheme.dll for dark mode support
using fnSetPreferredAppMode = PreferredAppMode(WINAPI*)(PreferredAppMode appMode);
using fnAllowDarkModeForWindow = bool(WINAPI*)(HWND hWnd, bool allow);
using fnShouldAppsUseDarkMode = bool(WINAPI*)();
using fnRefreshImmersiveColorPolicyState = void(WINAPI*)();
using fnFlushMenuThemes = void(WINAPI*)();
static fnSetPreferredAppMode pSetPreferredAppMode = nullptr;
static fnAllowDarkModeForWindow pAllowDarkModeForWindow = nullptr;
static fnShouldAppsUseDarkMode pShouldAppsUseDarkMode = nullptr;
static fnRefreshImmersiveColorPolicyState pRefreshImmersiveColorPolicyState = nullptr;
static fnFlushMenuThemes pFlushMenuThemes = nullptr;
//----------------------------------------------------------------------------
//
// InitializeDarkModeSupport
//
// Initialize dark mode function pointers from uxtheme.dll
//
//----------------------------------------------------------------------------
static void InitializeDarkModeSupport()
{
if (g_darkModeInitialized)
return;
g_darkModeInitialized = true;
HMODULE hUxTheme = GetModuleHandleW(L"uxtheme.dll");
if (hUxTheme)
{
// These are undocumented ordinal exports
// Ordinal 135: SetPreferredAppMode (Windows 10 1903+)
pSetPreferredAppMode = reinterpret_cast<fnSetPreferredAppMode>(
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(135)));
// Ordinal 133: AllowDarkModeForWindow
pAllowDarkModeForWindow = reinterpret_cast<fnAllowDarkModeForWindow>(
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(133)));
// Ordinal 132: ShouldAppsUseDarkMode
pShouldAppsUseDarkMode = reinterpret_cast<fnShouldAppsUseDarkMode>(
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(132)));
// Ordinal 104: RefreshImmersiveColorPolicyState
pRefreshImmersiveColorPolicyState = reinterpret_cast<fnRefreshImmersiveColorPolicyState>(
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(104)));
// Ordinal 136: FlushMenuThemes
pFlushMenuThemes = reinterpret_cast<fnFlushMenuThemes>(
GetProcAddress(hUxTheme, MAKEINTRESOURCEA(136)));
// Set preferred app mode based on our theme override or system setting
// Note: We check g_ThemeOverride directly here because IsDarkModeEnabled
// calls InitializeDarkModeSupport, which would cause recursion
if (pSetPreferredAppMode)
{
bool useDarkMode = false;
if (g_ThemeOverride == 0)
{
useDarkMode = false; // Force light
}
else if (g_ThemeOverride == 1)
{
useDarkMode = true; // Force dark
}
else if (pShouldAppsUseDarkMode)
{
useDarkMode = pShouldAppsUseDarkMode(); // Use system setting
}
if (useDarkMode)
{
pSetPreferredAppMode(PreferredAppMode::ForceDark);
}
else
{
pSetPreferredAppMode(PreferredAppMode::ForceLight);
}
}
// Flush menu themes to apply dark mode to context menus
if (pFlushMenuThemes)
{
pFlushMenuThemes();
}
}
// Update cached dark mode state
g_darkModeEnabled = false;
if (g_ThemeOverride == 0)
{
g_darkModeEnabled = false;
}
else if (g_ThemeOverride == 1)
{
g_darkModeEnabled = true;
}
else if (pShouldAppsUseDarkMode)
{
g_darkModeEnabled = pShouldAppsUseDarkMode();
}
}
//----------------------------------------------------------------------------
//
// IsDarkModeEnabled
//
//----------------------------------------------------------------------------
bool IsDarkModeEnabled()
{
// Check for theme override from registry (0=light, 1=dark, 2+=system)
if (g_ThemeOverride == 0)
{
return false; // Force light mode
}
else if (g_ThemeOverride == 1)
{
return true; // Force dark mode
}
InitializeDarkModeSupport();
// Check the undocumented API first
if (pShouldAppsUseDarkMode)
{
return pShouldAppsUseDarkMode();
}
// Fallback: Check registry for system theme preference
HKEY hKey;
if (RegOpenKeyExW(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0, KEY_READ, &hKey) == ERROR_SUCCESS)
{
DWORD value = 1;
DWORD size = sizeof(value);
RegQueryValueExW(hKey, L"AppsUseLightTheme", nullptr, nullptr,
reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
return value == 0; // 0 = dark mode, 1 = light mode
}
return false;
}
//----------------------------------------------------------------------------
//
// RefreshDarkModeState
//
//----------------------------------------------------------------------------
void RefreshDarkModeState()
{
InitializeDarkModeSupport();
if (pRefreshImmersiveColorPolicyState)
{
pRefreshImmersiveColorPolicyState();
}
// Update preferred app mode based on our IsDarkModeEnabled (respects override)
bool useDark = IsDarkModeEnabled();
if (pSetPreferredAppMode)
{
if (useDark)
{
pSetPreferredAppMode(PreferredAppMode::ForceDark);
}
else
{
pSetPreferredAppMode(PreferredAppMode::ForceLight);
}
}
// Flush menu themes to apply dark mode to context menus
if (pFlushMenuThemes)
{
pFlushMenuThemes();
}
g_darkModeEnabled = useDark;
}
//----------------------------------------------------------------------------
//
// SetDarkModeForWindow
//
//----------------------------------------------------------------------------
void SetDarkModeForWindow(HWND hWnd, bool enable)
{
InitializeDarkModeSupport();
if (pAllowDarkModeForWindow)
{
pAllowDarkModeForWindow(hWnd, enable);
}
// Use DWMWA_USE_IMMERSIVE_DARK_MODE attribute (Windows 10 build 17763+)
// Attribute 20 is DWMWA_USE_IMMERSIVE_DARK_MODE
BOOL useDarkMode = enable ? TRUE : FALSE;
HMODULE hDwmapi = GetModuleHandleW(L"dwmapi.dll");
if (hDwmapi)
{
using fnDwmSetWindowAttribute = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD);
auto pDwmSetWindowAttribute = reinterpret_cast<fnDwmSetWindowAttribute>(
GetProcAddress(hDwmapi, "DwmSetWindowAttribute"));
if (pDwmSetWindowAttribute)
{
// Try attribute 20 first (Windows 11 / newer Windows 10)
HRESULT hr = pDwmSetWindowAttribute(hWnd, 20, &useDarkMode, sizeof(useDarkMode));
if (FAILED(hr))
{
// Fall back to attribute 19 (older Windows 10)
pDwmSetWindowAttribute(hWnd, 19, &useDarkMode, sizeof(useDarkMode));
}
}
}
}
//----------------------------------------------------------------------------
//
// GetDarkModeBrush / GetDarkModeControlBrush / GetDarkModeSurfaceBrush
//
//----------------------------------------------------------------------------
HBRUSH GetDarkModeBrush()
{
if (!g_darkBackgroundBrush)
{
g_darkBackgroundBrush = CreateSolidBrush(DarkMode::BackgroundColor);
}
return g_darkBackgroundBrush;
}
HBRUSH GetDarkModeControlBrush()
{
if (!g_darkControlBrush)
{
g_darkControlBrush = CreateSolidBrush(DarkMode::ControlColor);
}
return g_darkControlBrush;
}
HBRUSH GetDarkModeSurfaceBrush()
{
if (!g_darkSurfaceBrush)
{
g_darkSurfaceBrush = CreateSolidBrush(DarkMode::SurfaceColor);
}
return g_darkSurfaceBrush;
}
//----------------------------------------------------------------------------
//
// ApplyDarkModeToDialog
//
//----------------------------------------------------------------------------
void ApplyDarkModeToDialog(HWND hDlg)
{
if (IsDarkModeEnabled())
{
SetDarkModeForWindow(hDlg, true);
// Set dark theme for the dialog
SetWindowTheme(hDlg, L"DarkMode_Explorer", nullptr);
// Apply dark theme to common controls (buttons, edit boxes, etc.)
EnumChildWindows(hDlg, [](HWND hChild, LPARAM) -> BOOL {
wchar_t className[64] = { 0 };
GetClassNameW(hChild, className, _countof(className));
// Apply appropriate theme based on control type
if (_wcsicmp(className, L"Button") == 0)
{
// Check if this is a checkbox or radio button
LONG style = GetWindowLong(hChild, GWL_STYLE);
LONG buttonType = style & BS_TYPEMASK;
if (buttonType == BS_CHECKBOX || buttonType == BS_AUTOCHECKBOX ||
buttonType == BS_3STATE || buttonType == BS_AUTO3STATE ||
buttonType == BS_RADIOBUTTON || buttonType == BS_AUTORADIOBUTTON)
{
// Subclass checkbox/radio for dark mode painting - but keep DarkMode_Explorer theme
// for proper hit testing (empty theme can break mouse interaction)
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
SetWindowSubclass(hChild, CheckboxSubclassProc, 2, 0);
}
else if (buttonType == BS_GROUPBOX)
{
// Subclass group box for dark mode painting
SetWindowTheme(hChild, L"", L"");
SetWindowSubclass(hChild, GroupBoxSubclassProc, 4, 0);
}
else
{
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
}
}
else if (_wcsicmp(className, L"Edit") == 0)
{
// Use empty theme and subclass for dark mode border drawing
SetWindowTheme(hChild, L"", L"");
SetWindowSubclass(hChild, EditControlSubclassProc, 3, 0);
}
else if (_wcsicmp(className, L"ComboBox") == 0)
{
SetWindowTheme(hChild, L"DarkMode_CFD", nullptr);
}
else if (_wcsicmp(className, L"SysListView32") == 0 ||
_wcsicmp(className, L"SysTreeView32") == 0)
{
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
}
else if (_wcsicmp(className, L"msctls_trackbar32") == 0)
{
// Subclass trackbar controls for dark mode painting
SetWindowTheme(hChild, L"", L"");
SetWindowSubclass(hChild, SliderSubclassProc, 1, 0);
}
else if (_wcsicmp(className, L"SysTabControl32") == 0)
{
// Use empty theme for tab control to allow dark background
SetWindowTheme(hChild, L"", L"");
}
else if (_wcsicmp(className, L"msctls_updown32") == 0)
{
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
}
else if (_wcsicmp(className, L"msctls_hotkey32") == 0)
{
// Subclass hotkey controls for dark mode painting
SetWindowTheme(hChild, L"", L"");
SetWindowSubclass(hChild, HotkeyControlSubclassProc, 1, 0);
}
else if (_wcsicmp(className, L"Static") == 0)
{
// Check if this is a text label (not an owner-draw or image control)
LONG style = GetWindowLong(hChild, GWL_STYLE);
LONG staticType = style & SS_TYPEMASK;
// Options header uses a dedicated static subclass (to support large title font).
// Avoid applying the generic static subclass on top of it.
const int controlId = GetDlgCtrlID( hChild );
if( controlId == IDC_VERSION || controlId == IDC_COPYRIGHT )
{
SetWindowTheme( hChild, L"", L"" );
return TRUE;
}
if (staticType == SS_LEFT || staticType == SS_CENTER || staticType == SS_RIGHT ||
staticType == SS_LEFTNOWORDWRAP || staticType == SS_SIMPLE)
{
// Subclass text labels for proper dark mode painting
SetWindowTheme(hChild, L"", L"");
SetWindowSubclass(hChild, StaticTextSubclassProc, 5, 0);
}
else
{
// Other static controls (icons, bitmaps, frames) - just remove theme
SetWindowTheme(hChild, L"", L"");
}
}
else
{
SetWindowTheme(hChild, L"DarkMode_Explorer", nullptr);
}
return TRUE;
}, 0);
}
else
{
// Light mode - remove dark mode
SetDarkModeForWindow(hDlg, false);
SetWindowTheme(hDlg, nullptr, nullptr);
EnumChildWindows(hDlg, [](HWND hChild, LPARAM) -> BOOL {
// Remove subclass from controls
wchar_t className[64] = { 0 };
GetClassNameW(hChild, className, _countof(className));
if (_wcsicmp(className, L"msctls_hotkey32") == 0)
{
RemoveWindowSubclass(hChild, HotkeyControlSubclassProc, 1);
}
else if (_wcsicmp(className, L"msctls_trackbar32") == 0)
{
RemoveWindowSubclass(hChild, SliderSubclassProc, 1);
}
else if (_wcsicmp(className, L"Button") == 0)
{
LONG style = GetWindowLong(hChild, GWL_STYLE);
LONG buttonType = style & BS_TYPEMASK;
if (buttonType == BS_CHECKBOX || buttonType == BS_AUTOCHECKBOX ||
buttonType == BS_3STATE || buttonType == BS_AUTO3STATE ||
buttonType == BS_RADIOBUTTON || buttonType == BS_AUTORADIOBUTTON)
{
RemoveWindowSubclass(hChild, CheckboxSubclassProc, 2);
}
else if (buttonType == BS_GROUPBOX)
{
RemoveWindowSubclass(hChild, GroupBoxSubclassProc, 4);
}
}
else if (_wcsicmp(className, L"Edit") == 0)
{
RemoveWindowSubclass(hChild, EditControlSubclassProc, 3);
}
else if (_wcsicmp(className, L"Static") == 0)
{
RemoveWindowSubclass(hChild, StaticTextSubclassProc, 5);
}
SetWindowTheme(hChild, nullptr, nullptr);
return TRUE;
}, 0);
}
}
//----------------------------------------------------------------------------
//
// HandleDarkModeCtlColor
//
//----------------------------------------------------------------------------
HBRUSH HandleDarkModeCtlColor(HDC hdc, HWND hCtrl, UINT message)
{
if (!IsDarkModeEnabled())
{
return nullptr;
}
switch (message)
{
case WM_CTLCOLORDLG:
SetBkColor(hdc, DarkMode::BackgroundColor);
SetTextColor(hdc, DarkMode::TextColor);
return GetDarkModeBrush();
case WM_CTLCOLORSTATIC:
SetBkMode(hdc, TRANSPARENT);
// Use dimmed color for disabled static controls
if (!IsWindowEnabled(hCtrl))
{
SetTextColor(hdc, RGB(100, 100, 100));
}
else
{
SetTextColor(hdc, DarkMode::TextColor);
}
return GetDarkModeBrush();
case WM_CTLCOLORBTN:
SetBkColor(hdc, DarkMode::ControlColor);
SetTextColor(hdc, DarkMode::TextColor);
return GetDarkModeControlBrush();
case WM_CTLCOLOREDIT:
SetBkColor(hdc, DarkMode::SurfaceColor);
SetTextColor(hdc, DarkMode::TextColor);
return GetDarkModeSurfaceBrush();
case WM_CTLCOLORLISTBOX:
SetBkColor(hdc, DarkMode::SurfaceColor);
SetTextColor(hdc, DarkMode::TextColor);
return GetDarkModeSurfaceBrush();
}
return nullptr;
}
//----------------------------------------------------------------------------
//
// ApplyDarkModeToMenu
//
// Uses undocumented uxtheme functions to enable dark mode for menus
//
//----------------------------------------------------------------------------
void ApplyDarkModeToMenu(HMENU hMenu)
{
if (!hMenu)
{
return;
}
if (!IsDarkModeEnabled())
{
// Light mode - clear any dark background
MENUINFO mi = { sizeof(mi) };
mi.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS;
mi.hbrBack = nullptr;
SetMenuInfo(hMenu, &mi);
return;
}
// For popup menus, we need to use MENUINFO to set the background
MENUINFO mi = { sizeof(mi) };
mi.fMask = MIM_BACKGROUND | MIM_APPLYTOSUBMENUS;
mi.hbrBack = GetDarkModeSurfaceBrush();
SetMenuInfo(hMenu, &mi);
}
//----------------------------------------------------------------------------
//
// RefreshWindowTheme
//
// Forces a window and all its children to redraw with current theme
//
//----------------------------------------------------------------------------
void RefreshWindowTheme(HWND hWnd)
{
if (!hWnd)
{
return;
}
// Reapply theme to this window
ApplyDarkModeToDialog(hWnd);
// Force redraw
RedrawWindow(hWnd, nullptr, nullptr, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_FRAME);
}
//----------------------------------------------------------------------------
//
// CleanupDarkModeResources
//
//----------------------------------------------------------------------------
void CleanupDarkModeResources()
{
if (g_darkBackgroundBrush)
{
DeleteObject(g_darkBackgroundBrush);
g_darkBackgroundBrush = nullptr;
}
if (g_darkControlBrush)
{
DeleteObject(g_darkControlBrush);
g_darkControlBrush = nullptr;
}
if (g_darkSurfaceBrush)
{
DeleteObject(g_darkSurfaceBrush);
g_darkSurfaceBrush = nullptr;
}
}
//----------------------------------------------------------------------------
//
// InitializeDarkMode
//
// Public wrapper to initialize dark mode support early in app startup
//
//----------------------------------------------------------------------------
void InitializeDarkMode()
{
InitializeDarkModeSupport();
}
//----------------------------------------------------------------------------
//
@@ -151,3 +724,177 @@ POINT ScalePointInRects( POINT point, const RECT& source, const RECT& target )
return { targetCenter.x + MulDiv( point.x - sourceCenter.x, targetSize.cx, sourceSize.cx ),
targetCenter.y + MulDiv( point.y - sourceCenter.y, targetSize.cy, sourceSize.cy ) };
}
//----------------------------------------------------------------------------
//
// ScaleDialogForDpi
//
// Scales a dialog and all its child controls for the specified DPI.
// oldDpi defaults to DPI_BASELINE (96) for initial scaling.
//
//----------------------------------------------------------------------------
void ScaleDialogForDpi( HWND hDlg, UINT newDpi, UINT oldDpi )
{
if( newDpi == oldDpi || newDpi == 0 || oldDpi == 0 )
{
return;
}
// With PerMonitorV2, Windows automatically scales dialogs (layout and fonts) when created.
// We only need to scale when moving between monitors with different DPIs.
// When oldDpi == DPI_BASELINE, this is initial creation and Windows already handled scaling.
if( oldDpi == DPI_BASELINE )
{
return;
}
// Scale the dialog window itself
RECT dialogRect;
GetWindowRect( hDlg, &dialogRect );
int dialogWidth = MulDiv( dialogRect.right - dialogRect.left, newDpi, oldDpi );
int dialogHeight = MulDiv( dialogRect.bottom - dialogRect.top, newDpi, oldDpi );
SetWindowPos( hDlg, nullptr, 0, 0, dialogWidth, dialogHeight, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE );
// Enumerate and scale all child controls
HWND hChild = GetWindow( hDlg, GW_CHILD );
while( hChild != nullptr )
{
RECT childRect;
GetWindowRect( hChild, &childRect );
MapWindowPoints( nullptr, hDlg, reinterpret_cast<LPPOINT>(&childRect), 2 );
int x = MulDiv( childRect.left, newDpi, oldDpi );
int y = MulDiv( childRect.top, newDpi, oldDpi );
int width = MulDiv( childRect.right - childRect.left, newDpi, oldDpi );
int height = MulDiv( childRect.bottom - childRect.top, newDpi, oldDpi );
SetWindowPos( hChild, nullptr, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE );
// Scale the font for the control
HFONT hFont = reinterpret_cast<HFONT>(SendMessage( hChild, WM_GETFONT, 0, 0 ));
if( hFont != nullptr )
{
LOGFONT lf{};
if( GetObject( hFont, sizeof(lf), &lf ) )
{
lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
HFONT hNewFont = CreateFontIndirect( &lf );
if( hNewFont )
{
SendMessage( hChild, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
// Note: The old font might be shared, so we don't delete it here
// The system will clean up fonts when the dialog is destroyed
}
}
}
hChild = GetWindow( hChild, GW_HWNDNEXT );
}
// Also scale the dialog's own font
HFONT hDialogFont = reinterpret_cast<HFONT>(SendMessage( hDlg, WM_GETFONT, 0, 0 ));
if( hDialogFont != nullptr )
{
LOGFONT lf{};
if( GetObject( hDialogFont, sizeof(lf), &lf ) )
{
lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
HFONT hNewFont = CreateFontIndirect( &lf );
if( hNewFont )
{
SendMessage( hDlg, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
}
}
}
}
//----------------------------------------------------------------------------
//
// ScaleChildControlsForDpi
//
// Scales a window's direct child controls (and their fonts) for the specified DPI.
// Unlike ScaleDialogForDpi, this does not resize the parent window itself.
//
// This is useful for child dialogs used as tab pages: the tab page window is
// already scaled when the parent options dialog is scaled, but the controls
// inside the page are not (because they are grandchildren of the options dialog).
//
//----------------------------------------------------------------------------
void ScaleChildControlsForDpi( HWND hParent, UINT newDpi, UINT oldDpi )
{
if( newDpi == oldDpi || newDpi == 0 || oldDpi == 0 )
{
return;
}
// With PerMonitorV2, Windows automatically scales dialogs (layout and fonts) when created.
// We only need to scale when moving between monitors with different DPIs.
// When oldDpi == DPI_BASELINE, this is initial creation and Windows already handled scaling.
if( oldDpi == DPI_BASELINE )
{
return;
}
HWND hChild = GetWindow( hParent, GW_CHILD );
while( hChild != nullptr )
{
RECT childRect;
GetWindowRect( hChild, &childRect );
MapWindowPoints( nullptr, hParent, reinterpret_cast<LPPOINT>(&childRect), 2 );
int x = MulDiv( childRect.left, newDpi, oldDpi );
int y = MulDiv( childRect.top, newDpi, oldDpi );
int width = MulDiv( childRect.right - childRect.left, newDpi, oldDpi );
int height = MulDiv( childRect.bottom - childRect.top, newDpi, oldDpi );
SetWindowPos( hChild, nullptr, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE );
// Scale the font for the control
HFONT hFont = reinterpret_cast<HFONT>(SendMessage( hChild, WM_GETFONT, 0, 0 ));
if( hFont != nullptr )
{
LOGFONT lf{};
if( GetObject( hFont, sizeof(lf), &lf ) )
{
lf.lfHeight = MulDiv( lf.lfHeight, newDpi, oldDpi );
HFONT hNewFont = CreateFontIndirect( &lf );
if( hNewFont )
{
SendMessage( hChild, WM_SETFONT, reinterpret_cast<WPARAM>(hNewFont), TRUE );
}
}
}
hChild = GetWindow( hChild, GW_HWNDNEXT );
}
}
//----------------------------------------------------------------------------
//
// HandleDialogDpiChange
//
// Handles WM_DPICHANGED message for dialogs. Call this from the dialog's
// WndProc when WM_DPICHANGED is received.
//
//----------------------------------------------------------------------------
void HandleDialogDpiChange( HWND hDlg, WPARAM wParam, LPARAM lParam, UINT& currentDpi )
{
UINT newDpi = HIWORD( wParam );
if( newDpi != currentDpi && newDpi != 0 )
{
const RECT* pSuggestedRect = reinterpret_cast<const RECT*>(lParam);
// Scale the dialog controls from the current DPI to the new DPI
ScaleDialogForDpi( hDlg, newDpi, currentDpi );
// Move and resize the dialog to the suggested rectangle
SetWindowPos( hDlg, nullptr,
pSuggestedRect->left,
pSuggestedRect->top,
pSuggestedRect->right - pSuggestedRect->left,
pSuggestedRect->bottom - pSuggestedRect->top,
SWP_NOZORDER | SWP_NOACTIVATE );
currentDpi = newDpi;
}
}

View File

@@ -9,6 +9,10 @@
#pragma once
#include "pch.h"
#include <uxtheme.h>
// DPI baseline for scaling calculations (dialog units are designed at 96 DPI)
constexpr UINT DPI_BASELINE = USER_DEFAULT_SCREEN_DPI;
RECT ForceRectInBounds( RECT rect, const RECT& bounds );
UINT GetDpiForWindowHelper( HWND window );
@@ -16,3 +20,86 @@ RECT GetMonitorRectFromCursor();
RECT RectFromPointsMinSize( POINT a, POINT b, LONG minSize );
int ScaleForDpi( int value, UINT dpi );
POINT ScalePointInRects( POINT point, const RECT& source, const RECT& target );
// Dialog DPI scaling functions
void ScaleDialogForDpi( HWND hDlg, UINT newDpi, UINT oldDpi = DPI_BASELINE );
void ScaleChildControlsForDpi( HWND hParent, UINT newDpi, UINT oldDpi = DPI_BASELINE );
void HandleDialogDpiChange( HWND hDlg, WPARAM wParam, LPARAM lParam, UINT& currentDpi );
//----------------------------------------------------------------------------
// Dark Mode Support
//----------------------------------------------------------------------------
// Dark mode colors
namespace DarkMode
{
// Background colors
constexpr COLORREF BackgroundColor = RGB(32, 32, 32);
constexpr COLORREF SurfaceColor = RGB(45, 45, 48);
constexpr COLORREF ControlColor = RGB(51, 51, 55);
// Text colors
constexpr COLORREF TextColor = RGB(200, 200, 200);
constexpr COLORREF DisabledTextColor = RGB(120, 120, 120);
constexpr COLORREF LinkColor = RGB(86, 156, 214);
// Border/accent colors
constexpr COLORREF BorderColor = RGB(67, 67, 70);
constexpr COLORREF AccentColor = RGB(0, 120, 215);
constexpr COLORREF HoverColor = RGB(62, 62, 66);
// Light mode colors for contrast
constexpr COLORREF LightBackgroundColor = RGB(255, 255, 255);
constexpr COLORREF LightTextColor = RGB(0, 0, 0);
}
// Check if system dark mode is enabled
bool IsDarkModeEnabled();
// Refresh dark mode state (call when WM_SETTINGCHANGE received)
void RefreshDarkModeState();
// Enable dark mode title bar for a window
void SetDarkModeForWindow(HWND hWnd, bool enable);
// Apply dark mode to a dialog and enable dark title bar
void ApplyDarkModeToDialog(HWND hDlg);
// Get the appropriate background brush for dark/light mode
HBRUSH GetDarkModeBrush();
HBRUSH GetDarkModeControlBrush();
HBRUSH GetDarkModeSurfaceBrush();
// Handle WM_CTLCOLOR* messages for dark mode
// Returns the brush to use, or nullptr if default handling should be used
HBRUSH HandleDarkModeCtlColor(HDC hdc, HWND hCtrl, UINT message);
// Apply dark mode theme to a popup menu
void ApplyDarkModeToMenu(HMENU hMenu);
// Force redraw of a window and all its children for theme change
void RefreshWindowTheme(HWND hWnd);
// Cleanup dark mode resources (call at app exit)
void CleanupDarkModeResources();
// Initialize dark mode support early in app startup (call before creating windows)
void InitializeDarkMode();
// Subclass procedure for hotkey controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK HotkeyControlSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
// Subclass procedure for checkbox controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK CheckboxSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
// Subclass procedure for edit controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK EditControlSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
// Subclass procedure for group box controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK GroupBoxSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
// Subclass procedure for slider/trackbar controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK SliderSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
// Subclass procedure for static text controls - needs to be accessible from Utility.cpp
LRESULT CALLBACK StaticTextSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,12 @@
#include "CaptureFrameWait.h"
#include "AudioSampleGenerator.h"
#include <d3d11_4.h>
#include <ppltasks.h>
#include <atomic>
#include <algorithm>
#include <chrono>
#include <mutex>
#include <vector>
class VideoRecordingSession : public std::enable_shared_from_this<VideoRecordingSession>
{
@@ -21,6 +27,7 @@ public:
RECT const& cropRect,
uint32_t frameRate,
bool captureAudio,
bool captureSystemAudio,
winrt::Streams::IRandomAccessStream const& stream);
~VideoRecordingSession();
@@ -28,6 +35,151 @@ public:
void EnableCursorCapture(bool enable = true) { m_frameWait->EnableCursorCapture(enable); }
void Close();
bool HasCapturedVideoFrames() const { return m_hasVideoSample.load(); }
// Trim and save functionality
static std::wstring ShowSaveDialogWithTrim(
HWND hWnd,
const std::wstring& suggestedFileName,
const std::wstring& originalVideoPath,
std::wstring& trimmedVideoPath);
struct TrimDialogData
{
struct GifFrame
{
HBITMAP hBitmap{ nullptr };
winrt::Windows::Foundation::TimeSpan start{ 0 };
winrt::Windows::Foundation::TimeSpan duration{ 0 };
UINT width{ 0 };
UINT height{ 0 };
};
std::wstring videoPath;
winrt::Windows::Foundation::TimeSpan videoDuration{ 0 };
winrt::Windows::Foundation::TimeSpan trimStart{ 0 };
winrt::Windows::Foundation::TimeSpan trimEnd{ 0 };
winrt::Windows::Foundation::TimeSpan originalTrimStart{ 0 }; // Initial value to detect if trim needed
winrt::Windows::Foundation::TimeSpan originalTrimEnd{ 0 }; // Initial value to detect if trim needed
winrt::Windows::Foundation::TimeSpan currentPosition{ 0 };
// Playback loop anchor. This is set when the user explicitly positions the playhead
// (e.g., dragging or using the jog buttons). Pausing/resuming should not change it.
winrt::Windows::Foundation::TimeSpan playbackStartPosition{ 0 };
bool playbackStartPositionValid{ false };
// Cached preview frame at playback start position for instant restore when playback stops.
HBITMAP hCachedStartFrame{ nullptr };
winrt::Windows::Foundation::TimeSpan cachedStartFramePosition{ -1 };
// When starting playback at a non-zero position, MediaPlayer may briefly report Position==0
// before the initial seek is applied. Use this to suppress a one-frame UI jump to 0.
std::atomic<bool> pendingInitialSeek{ false };
std::atomic<int64_t> pendingInitialSeekTicks{ 0 };
winrt::Windows::Media::Editing::MediaComposition composition{ nullptr };
winrt::Windows::Media::Playback::MediaPlayer mediaPlayer{ nullptr };
winrt::Windows::Storage::StorageFile playbackFile{ nullptr };
HBITMAP hPreviewBitmap{ nullptr };
HWND hDialog{ nullptr };
std::atomic<bool> loadingPreview{ false };
std::atomic<int64_t> latestPreviewRequest{ 0 };
std::atomic<int64_t> lastRenderedPreview{ -1 };
std::atomic<bool> isPlaying{ false };
// Monotonic serial used to cancel in-flight StartPlaybackAsync work when the user
// immediately pauses after starting playback.
std::atomic<uint64_t> playbackCommandSerial{ 0 };
std::atomic<bool> frameCopyInProgress{ false };
std::atomic<bool> smoothActive{ false };
std::atomic<int64_t> smoothBaseTicks{ 0 };
std::atomic<int64_t> smoothLastSyncMicroseconds{ 0 };
std::atomic<bool> smoothHasNonZeroSample{ false };
std::mutex previewBitmapMutex;
winrt::event_token frameAvailableToken{};
winrt::event_token positionChangedToken{};
winrt::event_token stateChangedToken{};
winrt::com_ptr<ID3D11Device> previewD3DDevice;
winrt::com_ptr<ID3D11DeviceContext> previewD3DContext;
winrt::com_ptr<ID3D11Texture2D> previewFrameTexture;
winrt::com_ptr<ID3D11Texture2D> previewFrameStaging;
bool hoverPlay{ false };
bool hoverRewind{ false };
bool hoverForward{ false };
bool hoverSkipStart{ false };
bool hoverSkipEnd{ false };
bool hoverVolumeIcon{ false };
double volume{ 0.70 }; // Volume level 0.0 to 1.0, initialized from g_TrimDialogVolume in dialog init
double previousVolume{ 0.70 }; // Volume before muting, for unmute restoration
winrt::Windows::Foundation::TimeSpan previewOverride{ 0 };
winrt::Windows::Foundation::TimeSpan positionBeforeOverride{ 0 };
bool previewOverrideActive{ false };
bool restorePreviewOnRelease{ false };
bool playheadPushed{ false };
int dialogX{ 0 };
int dialogY{ 0 };
bool isGif{ false };
bool previewBitmapOwned{ true };
std::vector<GifFrame> gifFrames;
bool gifFramesLoaded{ false };
size_t gifLastFrameIndex{ 0 };
std::chrono::steady_clock::time_point gifFrameStartTime{}; // When the current GIF frame started displaying
// Font for time labels
HFONT hTimeLabelFont{ nullptr };
// Mouse tracking for timeline
enum DragMode { None, TrimStart, Position, TrimEnd };
DragMode dragMode{ None };
bool isDragging{ false };
int lastPlayheadX{ -1 }; // Track last playhead pixel position for efficient invalidation
MMRESULT mmTimerId{ 0 }; // Multimedia timer for smooth MP4 playback
// Helper to convert time to pixel position
int TimeToPixel(winrt::Windows::Foundation::TimeSpan time, int timelineWidth) const
{
if (timelineWidth <= 0 || videoDuration.count() <= 0)
{
return 0;
}
double ratio = static_cast<double>(time.count()) / static_cast<double>(videoDuration.count());
ratio = std::clamp(ratio, 0.0, 1.0);
return static_cast<int>(ratio * timelineWidth);
}
// Helper to convert pixel to time
winrt::Windows::Foundation::TimeSpan PixelToTime(int pixel, int timelineWidth) const
{
if (timelineWidth <= 0 || videoDuration.count() <= 0)
{
return winrt::Windows::Foundation::TimeSpan{ 0 };
}
int clampedPixel = std::clamp(pixel, 0, timelineWidth);
double ratio = static_cast<double>(clampedPixel) / static_cast<double>(timelineWidth);
return winrt::Windows::Foundation::TimeSpan{ static_cast<int64_t>(ratio * videoDuration.count()) };
}
};
static INT_PTR ShowTrimDialog(
HWND hParent,
const std::wstring& videoPath,
winrt::Windows::Foundation::TimeSpan& trimStart,
winrt::Windows::Foundation::TimeSpan& trimEnd);
private:
static INT_PTR CALLBACK TrimDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
static winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> TrimVideoAsync(
const std::wstring& sourceVideoPath,
winrt::Windows::Foundation::TimeSpan trimTimeStart,
winrt::Windows::Foundation::TimeSpan trimTimeEnd);
static winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> TrimGifAsync(
const std::wstring& sourceGifPath,
winrt::Windows::Foundation::TimeSpan trimTimeStart,
winrt::Windows::Foundation::TimeSpan trimTimeEnd);
static INT_PTR ShowTrimDialogInternal(
HWND hParent,
const std::wstring& videoPath,
winrt::Windows::Foundation::TimeSpan& trimStart,
winrt::Windows::Foundation::TimeSpan& trimEnd);
private:
VideoRecordingSession(
winrt::Direct3D11::IDirect3DDevice const& device,
@@ -35,6 +187,7 @@ private:
RECT const cropRect,
uint32_t frameRate,
bool captureAudio,
bool captureSystemAudio,
winrt::Streams::IRandomAccessStream const& stream);
void CloseInternal();
@@ -68,4 +221,7 @@ private:
std::atomic<bool> m_isRecording = false;
std::atomic<bool> m_closed = false;
// Set once the MediaStreamSource successfully returns at least one video sample.
std::atomic<bool> m_hasVideoSample = false;
};

View File

@@ -32,18 +32,18 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
// TEXTINCLUDE
//
1 TEXTINCLUDE
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
3 TEXTINCLUDE
BEGIN
"#include ""binres.rc""\0"
END
@@ -113,26 +113,26 @@ END
// Dialog
//
OPTIONS DIALOGEX 0, 0, 279, 325
OPTIONS DIALOGEX 0, 0, 299, 325
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CLIPSIBLINGS | WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_CONTROLPARENT
CAPTION "ZoomIt - Sysinternals: www.sysinternals.com"
FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
LTEXT "ZoomIt v9.21",IDC_VERSION,42,7,73,10
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
DEFPUSHBUTTON "OK",IDOK,186,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,243,306,50,14
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
ICON "APPICON",IDC_STATIC,12,9,20,20
CONTROL "Show tray icon",IDC_SHOW_TRAY_ICON,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,295,105,10
CONTROL "",IDC_TAB,"SysTabControl32",TCS_MULTILINE | WS_TABSTOP,8,46,265,245
CONTROL "",IDC_TAB,"SysTabControl32",TCS_MULTILINE | WS_TABSTOP,8,46,285,247
CONTROL "Run ZoomIt when Windows starts",IDC_AUTOSTART,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,309,122,10
END
ADVANCED_BREAK DIALOGEX 0, 0, 209, 219
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
ADVANCED_BREAK DIALOGEX 0, 0, 209, 225
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Advanced Break Options"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
@@ -158,23 +158,22 @@ BEGIN
EDITTEXT IDC_BACKGROUND_FILE,62,164,125,12,ES_AUTOHSCROLL | ES_READONLY
PUSHBUTTON "&...",IDC_BACKGROUND_BROWSE,188,164,13,11
CONTROL "Scale to screen:",IDC_CHECK_BACKGROUND_STRETCH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,58,180,67,10,WS_EX_RIGHT
DEFPUSHBUTTON "OK",IDOK,97,201,50,14
PUSHBUTTON "Cancel",IDCANCEL,150,201,50,14
DEFPUSHBUTTON "OK",IDOK,97,199,50,14
PUSHBUTTON "Cancel",IDCANCEL,150,199,50,14
LTEXT "Alarm Sound File:",IDC_STATIC_SOUND_FILE,61,26,56,8
LTEXT "Timer Opacity:",IDC_STATIC,8,59,48,8
LTEXT "Timer Position:",IDC_STATIC,8,77,48,8
CONTROL "",IDC_STATIC,"Static",SS_BLACKFRAME | SS_SUNKEN,7,196,193,1,WS_EX_CLIENTEDGE
END
ZOOM DIALOGEX 0, 0, 260, 170
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,59,57,80,12
LTEXT "After toggling ZoomIt you can zoom in with the mouse wheel or up and down arrow keys. Exit zoom mode with Escape or by pressing the right mouse button.",IDC_STATIC,7,6,246,26
LTEXT "After toggling ZoomIt you can zoom in with the mouse wheel or up and down arrow keys. Exit zoom mode with Escape or by pressing the right mouse button.",IDC_STATIC,7,6,230,26
LTEXT "Zoom Toggle:",IDC_STATIC,7,59,51,8
CONTROL "",IDC_ZOOM_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,53,118,150,15,WS_EX_TRANSPARENT
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,105,215,10
LTEXT "Specify the initial level of magnification when zooming in:",IDC_STATIC,7,105,230,10
LTEXT "1.25",IDC_STATIC,52,136,16,8
LTEXT "1.5",IDC_STATIC,82,136,12,8
LTEXT "1.75",IDC_STATIC,108,136,16,8
@@ -183,52 +182,52 @@ BEGIN
LTEXT "4.0",IDC_STATIC,190,136,12,8
CONTROL "Animate zoom in and zoom out:",IDC_ANIMATE_ZOOM,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,74,116,10
CONTROL "Smooth zoomed image:",IDC_SMOOTH_IMAGE,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,88,116,10
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,148,246,17
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,6,34,246,18
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,7,148,230,17
LTEXT "Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,6,34,230,18
END
DRAW DIALOGEX 0, 0, 260, 228
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
LTEXT "Once zoomed, toggle drawing mode by pressing the left mouse button. Undo with Ctrl+Z and all drawing by pressing E. Center the cursor with the space bar. Exit drawing mode by pressing the right mouse button.",IDC_STATIC,7,7,246,24
LTEXT "Once zoomed, toggle drawing mode by pressing the left mouse button. Undo with Ctrl+Z and all drawing by pressing E. Center the cursor with the space bar. Exit drawing mode by pressing the right mouse button.",IDC_STATIC,7,7,230,24
LTEXT "Pen Control ",IDC_PEN_CONTROL,7,38,40,8
LTEXT "Change the pen width by pressing left Ctrl and using the mouse wheel or the up and down arrow keys.",IDC_STATIC,19,48,233,16
LTEXT "Change the pen width by pressing left Ctrl and using the mouse wheel or the up and down arrow keys.",IDC_STATIC,19,48,218,16
LTEXT "Colors",IDC_COLORS,7,70,21,8
LTEXT "Change the pen color by pressing R (red), G (green), B (blue),\nO (orange), Y (yellow) or P (pink).",IDC_STATIC,19,80,233,16
LTEXT "Change the pen color by pressing R (red), G (green), B (blue),\nO (orange), Y (yellow) or P (pink).",IDC_STATIC,19,80,218,16
LTEXT "Highlight and Blur",IDC_HIGHLIGHT_AND_BLUR,7,102,58,8
LTEXT "Hold Shift while pressing a color key for a translucent highlighter color. Press X for blur or Shift+X for a stronger blur.",IDC_STATIC,19,113,233,16
LTEXT "Hold Shift while pressing a color key for a translucent highlighter color. Press X for blur or Shift+X for a stronger blur.",IDC_STATIC,19,113,218,16
LTEXT "Shapes",IDC_SHAPES,7,134,23,8
LTEXT "Draw a line by holding down the Shift key, a rectangle with the Ctrl key, an ellipse with the Tab key and an arrow with Shift+Ctrl.",IDC_STATIC,19,144,233,16
LTEXT "Draw a line by holding down the Shift key, a rectangle with the Ctrl key, an ellipse with the Tab key and an arrow with Shift+Ctrl.",IDC_STATIC,19,144,218,16
LTEXT "Screen",IDC_SCREEN,7,166,22,8
LTEXT "Clear the screen for a sketch pad by pressing W (white) or K (black). Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,19,176,233,24
LTEXT "Clear the screen for a sketch pad by pressing W (white) or K (black). Copy a zoomed screen with Ctrl+C or save it by typing Ctrl+S. Crop the copy or save region by entering Ctrl+Shift instead of Ctrl.",IDC_STATIC,19,176,218,24
CONTROL "",IDC_DRAW_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,73,207,80,12
LTEXT "Draw w/out Zoom:",IDC_STATIC,7,210,63,11
END
TYPE DIALOGEX 0, 0, 260, 104
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
LTEXT "Once in drawing mode, type 't' to enter typing mode or shift+'t' to enter typing mode with right-aligned input. Exit typing mode by pressing escape or the left mouse button. Use the mouse wheel or up and down arrow keys to change the font size.",IDC_STATIC,7,7,246,32
LTEXT "The text color is the current drawing color.",IDC_STATIC,7,47,211,9
LTEXT "Once in drawing mode, type 't' to enter typing mode or shift+'t' to enter typing mode with right-aligned input. Exit typing mode by pressing escape or the left mouse button. Use the mouse wheel or up and down arrow keys to change the font size.",IDC_STATIC,7,7,230,32
LTEXT "The text color is the current drawing color.",IDC_STATIC,7,47,230,9
PUSHBUTTON "&Font",IDC_FONT,112,69,41,14
GROUPBOX "Text Font",IDC_TEXT_FONT,8,61,99,28
GROUPBOX "Sample",IDC_TEXT_FONT,8,61,99,28
END
BREAK DIALOGEX 0, 0, 260, 123
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_BREAK_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,52,67,80,12
EDITTEXT IDC_TIMER,31,86,31,13,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER
CONTROL "",IDC_SPIN_TIMER,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,45,86,11,12
LTEXT "minutes",IDC_STATIC,67,88,25,8
PUSHBUTTON "&Advanced",IDC_ADVANCED_BREAK,212,102,41,14
LTEXT "Enter timer mode by using the ZoomIt tray icon's Break menu item. Increase and decrease time with the arrow keys. If you Alt-Tab away from the timer window, reactivate it by left-clicking on the ZoomIt tray icon. Exit timer mode with Escape. ",IDC_STATIC,7,7,246,33
EDITTEXT IDC_TIMER,52,86,31,13,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER
CONTROL "",IDC_SPIN_TIMER,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,66,86,11,12
LTEXT "minutes",IDC_STATIC,88,88,25,8
PUSHBUTTON "&Advanced",IDC_ADVANCED_BREAK,192,102,41,14
LTEXT "Enter timer mode by using the ZoomIt tray icon's Break menu item. Increase and decrease time with the arrow keys. If you Alt-Tab away from the timer window, reactivate it by left-clicking on the ZoomIt tray icon. Exit timer mode with Escape. ",IDC_STATIC,7,7,230,33
LTEXT "Start Timer:",IDC_STATIC,7,70,39,8
LTEXT "Timer:",IDC_STATIC,7,88,20,8
LTEXT "Change the break timer color using the same keys that the drawing color. The break timer font is the same as text font.",IDC_STATIC,7,45,219,20
LTEXT "Change the break timer color using the same keys that the drawing color. The break timer font is the same as text font.",IDC_STATIC,7,45,230,20
CONTROL "Show Time Elapsed After Expiration:",IDC_CHECK_SHOW_EXPIRED,
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,8,104,132,10
END
@@ -251,69 +250,90 @@ BEGIN
END
LIVEZOOM DIALOGEX 0, 0, 260, 134
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_LIVE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,69,108,80,12
LTEXT "LiveZoom mode is supported on Windows 7 and higher where window updates show while zoomed. ",IDC_STATIC,7,7,246,18
LTEXT "LiveZoom mode is supported on Windows 7 and higher where window updates show while zoomed. ",IDC_STATIC,7,7,230,18
LTEXT "LiveZoom Toggle:",IDC_STATIC,7,110,62,8
LTEXT "To enter and exit LiveZoom, enter the hotkey specified below.",IDC_STATIC,7,94,218,13
LTEXT "Note that in LiveZoom you must use Ctrl+Up and Ctrl+Down to control the zoom level. To enter drawing mode, use the standard zoom-without-draw hotkey and then escape to go back to LiveZoom.",IDC_STATIC,7,30,246,27
LTEXT "Use LiveDraw to draw and annotate the live desktop. To activate LiveDraw, enter the hotkey with the Shift key in the opposite mode. You can remove LiveDraw annotations by activating LiveDraw and enter the escape key",IDC_STATIC,7,62,246,32
LTEXT "To enter and exit LiveZoom, enter the hotkey specified below.",IDC_STATIC,7,94,230,13
LTEXT "Note that in LiveZoom you must use Ctrl+Up and Ctrl+Down to control the zoom level. To enter drawing mode, use the standard zoom-without-draw hotkey and then escape to go back to LiveZoom.",IDC_STATIC,7,30,230,27
LTEXT "Use LiveDraw to draw and annotate the live desktop. To activate LiveDraw, enter the hotkey with the Shift key in the opposite mode. You can remove LiveDraw annotations by activating LiveDraw and enter the escape key",IDC_STATIC,7,62,230,32
END
RECORD DIALOGEX 0, 0, 260, 169
RECORD DIALOGEX 0, 0, 260, 181
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_RECORD_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,61,96,80,12
LTEXT "Record Toggle:",IDC_STATIC,7,98,54,8
LTEXT "Record video of the unzoomed live screen or a static zoomed session by entering the recording hot key and finish the recording by entering it again. ",IDC_STATIC,7,7,246,28
LTEXT "Note: Recording is only available on Windows 10 (version 1903) and higher.",IDC_STATIC,7,77,246,19
LTEXT "Record video of the unzoomed live screen or a static zoomed session by entering the recording hot key and finish the recording by entering it again. ",IDC_STATIC,7,7,248,28
LTEXT "Note: Recording is only available on Windows 10 (version 1903) and higher.",IDC_STATIC,7,77,249,19
LTEXT "Scaling:",IDC_STATIC,30,115,26,8
COMBOBOX IDC_RECORD_SCALING,61,114,26,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | WS_VSCROLL | WS_TABSTOP
LTEXT "Format:",IDC_STATIC,30,132,26,8
COMBOBOX IDC_RECORD_FORMAT,61,131,60,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | WS_VSCROLL | WS_TABSTOP
LTEXT "Frame Rate:",IDC_STATIC,119,115,44,8,NOT WS_VISIBLE
COMBOBOX IDC_RECORD_FRAME_RATE,166,114,42,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | NOT WS_VISIBLE | WS_VSCROLL | WS_TABSTOP
LTEXT "To crop the portion of the screen that will be recorded, enter the hotkey with the Shift key in the opposite mode. ",IDC_STATIC,7,32,246,19
LTEXT "To record a specific window, enter the hotkey with the Alt key in the opposite mode.",IDC_STATIC,7,55,246,19
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,149,83,10
COMBOBOX IDC_MICROPHONE,81,164,172,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
LTEXT "Microphone:",IDC_STATIC,32,166,47,8
LTEXT "To crop the portion of the screen that will be recorded, enter the hotkey with the Shift key in the opposite mode. ",IDC_STATIC,7,35,245,19
LTEXT "To record a specific window, enter the hotkey with the Alt key in the opposite mode.",IDC_STATIC,7,55,251,19
CONTROL "Capture &system audio",IDC_CAPTURE_SYSTEM_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,149,83,10
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,161,83,10
COMBOBOX IDC_MICROPHONE,81,176,152,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
LTEXT "Microphone:",IDC_MICROPHONE_LABEL,32,178,47,8
END
SNIP DIALOGEX 0, 0, 260, 68
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_SNIP_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,55,32,80,12
LTEXT "Snip Toggle:",IDC_STATIC,7,33,45,8
LTEXT "Copy a region of the screen to the clipboard or enter the hotkey with the Shift key in the opposite mode to save it to a file. ",IDC_STATIC,7,7,246,19
LTEXT "Copy a region of the screen to the clipboard or enter the hotkey with the Shift key in the opposite mode to save it to a file. ",IDC_STATIC,7,7,230,19
END
DEMOTYPE DIALOGEX 0, 0, 259, 249
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
DEMOTYPE DIALOGEX 0, 0, 260, 249
STYLE DS_SETFONT | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_SYSMENU
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
CONTROL "",IDC_DEMOTYPE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,74,154,80,12
LTEXT "DemoType toggle:",IDC_STATIC,7,157,63,8
PUSHBUTTON "&...",IDC_DEMOTYPE_BROWSE,231,137,16,13
PUSHBUTTON "&...",IDC_DEMOTYPE_BROWSE,211,137,16,13
CONTROL "",IDC_DEMOTYPE_SPEED_SLIDER,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,52,202,150,11,WS_EX_TRANSPARENT
CONTROL "Drive input with typing:",IDC_DEMOTYPE_USER_DRIVEN,
"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,173,88,10
LTEXT "DemoType typing speed:",IDC_STATIC,7,189,215,10
LTEXT "DemoType typing speed:",IDC_STATIC,7,189,230,10
LTEXT "Slow",IDC_DEMOTYPE_STATIC1,51,213,18,8
LTEXT "Fast",IDC_DEMOTYPE_STATIC2,186,213,17,8
EDITTEXT IDC_DEMOTYPE_FILE,44,137,187,12,ES_AUTOHSCROLL | ES_READONLY
EDITTEXT IDC_DEMOTYPE_FILE,44,137,167,12,ES_AUTOHSCROLL | ES_READONLY
LTEXT "Input file:",IDC_STATIC,7,139,32,8
LTEXT "When you reach the end of the file, ZoomIt will reload the file and start at the beginning. Enter the hotkey with the Shift key in the opposite mode to step back to the last [end].",IDC_STATIC,7,108,248,24
LTEXT "DemoType has ZoomIt type text specified in the input file when you enter the DemoType toggle. Simply separate snippets with the [end] keyword, or you can insert text from the clipboard if it is prefixed with the [start].",IDC_STATIC,7,7,248,24
LTEXT " - Insert pauses with the [pause:n] keyword where 'n' is seconds. ",IDC_STATIC,19,34,212,11
LTEXT "You can have ZoomIt send text automatically, or select the option to drive input with typing. ZoomIt will block keyboard input while sending output.",IDC_STATIC,7,68,248,16
LTEXT "When driving input, hit the space bar to unblock keyboard input at the end of a snippet. In auto mode, control will be returned upon completion.",IDC_STATIC,7,88,248,16
LTEXT "- Send text via the clipboard with [paste] and [/paste]. ",IDC_STATIC,23,45,178,8
LTEXT "- Send keystrokes with [enter], [up], [down], [left], and [right].",IDC_STATIC,23,56,211,8
LTEXT "When you reach the end of the file, ZoomIt will reload the file and start at the beginning. Enter the hotkey with the Shift key in the opposite mode to step back to the last [end].",IDC_STATIC,7,108,230,24
LTEXT "DemoType has ZoomIt type text specified in the input file when you enter the DemoType toggle. Simply separate snippets with the [end] keyword, or you can insert text from the clipboard if it is prefixed with the [start].",IDC_STATIC,7,7,230,24
LTEXT " - Insert pauses with the [pause:n] keyword where 'n' is seconds. ",IDC_STATIC,19,34,218,11
LTEXT "You can have ZoomIt send text automatically, or select the option to drive input with typing. ZoomIt will block keyboard input while sending output.",IDC_STATIC,7,68,230,16
LTEXT "When driving input, hit the space bar to unblock keyboard input at the end of a snippet. In auto mode, control will be returned upon completion.",IDC_STATIC,7,88,230,16
LTEXT "- Send text via the clipboard with [paste] and [/paste]. ",IDC_STATIC,23,45,210,8
LTEXT "- Send keystrokes with [enter], [up], [down], [left], and [right].",IDC_STATIC,23,56,210,8
END
IDD_VIDEO_TRIM DIALOGEX 0, 0, 521, 380
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
CAPTION "ZoomIt Video Trim"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
LTEXT "",IDC_TRIM_DURATION_LABEL,12,267,160,8
CONTROL "",IDC_TRIM_PREVIEW,"Static",SS_OWNERDRAW | SS_NOTIFY,12,12,498,244
CTEXT "00:00.000",IDC_TRIM_POSITION_LABEL,155,267,200,8
CONTROL "",IDC_TRIM_TIMELINE,"Static",SS_OWNERDRAW | SS_NOTIFY,11,277,498,47,WS_EX_TRANSPARENT
CONTROL "",IDC_TRIM_SKIP_START,"Button",BS_OWNERDRAW | WS_TABSTOP,183,327,30,26
CONTROL "",IDC_TRIM_REWIND,"Button",BS_OWNERDRAW | WS_TABSTOP,215,327,30,26
CONTROL "",IDC_TRIM_PLAY_PAUSE,"Button",BS_OWNERDRAW | WS_TABSTOP,247,325,44,32
CONTROL "",IDC_TRIM_FORWARD,"Button",BS_OWNERDRAW | WS_TABSTOP,293,327,30,26
CONTROL "",IDC_TRIM_SKIP_END,"Button",BS_OWNERDRAW | WS_TABSTOP,325,327,30,26
CONTROL "",IDC_TRIM_VOLUME_ICON,"Static",SS_OWNERDRAW | SS_NOTIFY,365,334,14,12
CONTROL "",IDC_TRIM_VOLUME,"msctls_trackbar32",TBS_NOTICKS | WS_TABSTOP,380,333,70,14
DEFPUSHBUTTON "OK",IDOK,404,358,50,14
PUSHBUTTON "Cancel",IDCANCEL,458,358,50,14
END
@@ -327,7 +347,7 @@ GUIDELINES DESIGNINFO
BEGIN
"OPTIONS", DIALOG
BEGIN
RIGHTMARGIN, 273
RIGHTMARGIN, 293
BOTTOMMARGIN, 320
END
@@ -340,7 +360,6 @@ BEGIN
"ZOOM", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 151
END
@@ -348,7 +367,6 @@ BEGIN
"DRAW", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 221
END
@@ -356,7 +374,6 @@ BEGIN
"TYPE", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 97
END
@@ -364,7 +381,6 @@ BEGIN
"BREAK", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 116
END
@@ -378,7 +394,6 @@ BEGIN
"LIVEZOOM", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 127
END
@@ -386,7 +401,6 @@ BEGIN
"RECORD", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 164
END
@@ -394,7 +408,6 @@ BEGIN
"SNIP", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 253
TOPMARGIN, 7
BOTTOMMARGIN, 61
END
@@ -402,10 +415,13 @@ BEGIN
"DEMOTYPE", DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 255
TOPMARGIN, 7
BOTTOMMARGIN, 205
END
IDD_VIDEO_TRIM, DIALOG
BEGIN
END
END
#endif // APSTUDIO_INVOKED
@@ -474,6 +490,11 @@ BEGIN
0
END
IDD_VIDEO_TRIM AFX_DIALOG_LAYOUT
BEGIN
0
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////

View File

@@ -216,6 +216,14 @@
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</MultiProcessorCompilation>
</ClCompile>
<ClCompile Include="LoopbackCapture.cpp">
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</MultiProcessorCompilation>
<MultiProcessorCompilation Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</MultiProcessorCompilation>
</ClCompile>
<ClCompile Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\dll.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
@@ -293,6 +301,7 @@
</ItemGroup>
<ItemGroup>
<ClInclude Include="AudioSampleGenerator.h" />
<ClInclude Include="LoopbackCapture.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\Eula\Eula.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)..\ZoomItModuleInterface\Trace.h" />
<ClInclude Include="GifRecordingSession.h" />

View File

@@ -33,6 +33,9 @@
<ClCompile Include="AudioSampleGenerator.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LoopbackCapture.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DemoType.cpp">
<Filter>Source Files</Filter>
</ClCompile>
@@ -80,6 +83,9 @@
<ClInclude Include="AudioSampleGenerator.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LoopbackCapture.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DemoType.h">
<Filter>Header Files</Filter>
</ClInclude>

View File

@@ -49,8 +49,15 @@ DWORD g_RecordScaling = 100;
DWORD g_RecordScalingGIF = 50;
DWORD g_RecordScalingMP4 = 100;
RecordingFormat g_RecordingFormat = RecordingFormat::MP4;
BOOLEAN g_CaptureSystemAudio = TRUE;
BOOLEAN g_CaptureAudio = FALSE;
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
TCHAR g_RecordingSaveLocationBuffer[MAX_PATH] = {0};
TCHAR g_ScreenshotSaveLocationBuffer[MAX_PATH] = {0};
DWORD g_ThemeOverride = 2; // 0=light, 1=dark, 2=system default
DWORD g_TrimDialogWidth = 0; // 0 means use default; stored in screen pixels
DWORD g_TrimDialogHeight = 0; // 0 means use default; stored in screen pixels
DWORD g_TrimDialogVolume = 70; // 0-100 volume level for trim dialog preview
REG_SETTING RegSettings[] = {
{ L"ToggleKey", SETTING_TYPE_DWORD, 0, &g_ToggleKey, static_cast<DOUBLE>(g_ToggleKey) },
@@ -91,6 +98,13 @@ REG_SETTING RegSettings[] = {
{ L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast<DOUBLE>(g_RecordScalingGIF) },
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },
{ L"CaptureSystemAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureSystemAudio, static_cast<DOUBLE>(g_CaptureSystemAudio) },
{ L"MicrophoneDeviceId", SETTING_TYPE_STRING, sizeof(g_MicrophoneDeviceId), g_MicrophoneDeviceId, static_cast<DOUBLE>(0) },
{ L"RecordingSaveLocation", SETTING_TYPE_STRING, sizeof(g_RecordingSaveLocationBuffer), g_RecordingSaveLocationBuffer, static_cast<DOUBLE>(0) },
{ L"ScreenshotSaveLocation", SETTING_TYPE_STRING, sizeof(g_ScreenshotSaveLocationBuffer), g_ScreenshotSaveLocationBuffer, static_cast<DOUBLE>(0) },
{ L"Theme", SETTING_TYPE_DWORD, 0, &g_ThemeOverride, static_cast<DOUBLE>(g_ThemeOverride) },
{ L"TrimDialogWidth", SETTING_TYPE_DWORD, 0, &g_TrimDialogWidth, static_cast<DOUBLE>(0) },
{ L"TrimDialogHeight", SETTING_TYPE_DWORD, 0, &g_TrimDialogHeight, static_cast<DOUBLE>(0) },
{ L"TrimDialogVolume", SETTING_TYPE_DWORD, 0, &g_TrimDialogVolume, static_cast<DOUBLE>(g_TrimDialogVolume) },
{ NULL, SETTING_TYPE_DWORD, 0, NULL, static_cast<DOUBLE>(0) }
};

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,10 @@
#include <shlobj.h>
#include <tchar.h>
#include <wincodec.h>
#include <shcore.h>
#include <magnification.h>
#include <Uxtheme.h>
#include <vssym32.h>
#include <math.h>
#include <shellapi.h>
#include <shlwapi.h>
@@ -41,12 +43,15 @@
#include <winrt/Windows.Graphics.DirectX.Direct3d11.h>
#include <winrt/Windows.Media.h>
#include <winrt/Windows.Media.Core.h>
#include <winrt/Windows.Media.Editing.h>
#include <winrt/Windows.Media.Playback.h>
#include <winrt/Windows.Media.Transcoding.h>
#include <winrt/Windows.Media.MediaProperties.h>
#include <winrt/Windows.Media.Devices.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Storage.Streams.h>
#include <winrt/Windows.Storage.Pickers.h>
#include <winrt/Windows.Storage.FileProperties.h>
#include <winrt/Windows.Devices.Enumeration.h>
#include <filesystem>
@@ -69,6 +74,9 @@
#include <d3d11_4.h>
#include <dxgi1_6.h>
#include <d2d1_3.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
// STL

View File

@@ -12,6 +12,7 @@
// Non-localizable
//////////////////////////////
#define IDC_AUDIO 117
#define IDD_VIDEO_TRIM 119
#define IDC_LINK 1000
#define IDC_ALT 1001
#define IDC_CTRL 1002
@@ -94,9 +95,22 @@
#define IDC_DEMOTYPE_STATIC2 1074
#define IDC_COPYRIGHT 1075
#define IDC_RECORD_FORMAT 1076
#define IDC_TRIM_POSITION_LABEL 1087
#define IDC_TRIM_PREVIEW 1088
#define IDC_TRIM_TIMELINE 1089
#define IDC_TRIM_PLAY_PAUSE 1090
#define IDC_TRIM_REWIND 1091
#define IDC_TRIM_FORWARD 1092
#define IDC_TRIM_DURATION_LABEL 1094
#define IDC_TRIM_SKIP_START 1095
#define IDC_TRIM_SKIP_END 1096
#define IDC_TRIM_VOLUME 1097
#define IDC_TRIM_VOLUME_ICON 1098
#define IDC_PEN_WIDTH 1105
#define IDC_TIMER 1106
#define IDC_SMOOTH_IMAGE 1107
#define IDC_CAPTURE_SYSTEM_AUDIO 1108
#define IDC_MICROPHONE_LABEL 1109
#define IDC_SAVE 40002
#define IDC_COPY 40004
#define IDC_RECORD 40006
@@ -109,9 +123,9 @@
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 118
#define _APS_NEXT_RESOURCE_VALUE 120
#define _APS_NEXT_COMMAND_VALUE 40013
#define _APS_NEXT_CONTROL_VALUE 1078
#define _APS_NEXT_CONTROL_VALUE 1099
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

View File

@@ -7,3 +7,4 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.DestroyMenuSafeHandle")]
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.FreeLibrarySafeHandle")]
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.UnhookWindowsHookExSafeHandle")]
[assembly: SuppressMessage("Interoperability", "CsWinRT1028: Class should be marked partial", Justification = "CsWin32 generated code; not used across WinRT boundary", Scope = "type", Target = "~T:Windows.Win32.DeleteObjectSafeHandle")]

View File

@@ -1,7 +1,7 @@
---
author: Mike Griese
created on: 2024-07-19
last updated: 2025-08-08
last updated: 2026-02-05
issue id: n/a
---
@@ -75,6 +75,8 @@ functionality.
- [Advanced scenarios](#advanced-scenarios)
- [Status messages](#status-messages)
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
- [Addenda I: API additions (ICommandProvider2)](#addenda-i-api-additions-icommandprovider2)
- [Addenda IV: Dock bands](#addenda-iv-dock-bands)
- [Class diagram](#class-diagram)
- [Future considerations](#future-considerations)
- [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments)
@@ -2045,6 +2047,87 @@ Fortunately, we can put all of that (`GetApiExtensionStubs`,
developers won't have to do anything. The toolkit will just do the right thing
for them.
## Addenda IV: Dock bands
The "dock" is another way to surface commands to the user. This is a
toolbar-like window that can be docked to the side of the screen, or floated as
its own window. It enables another surface for extensions to display real-time
information and shortcuts to users.
Bands are powered by the same interfaces as DevPal itself. Extensions can provide
bands via the new `DockBand` property on `ICommandProvider3`.
```csharp
interface ICommandProvider3 requires ICommandProvider2
{
ICommandItem[] GetDockBands();
};
```
A **Dock Band** is one "strip of items" in the dock. Each band can have multiple
items. This allows an extension to create a strip of buttons that should all be
treated as a single unit. For example, a media player band will want probably
four items:
* one for the previous track
* one for play/pause
* one for next track
* and one to display the album art and track title
`GetDockBands` returns an array of `ICommandItem`s. Each `ICommandItem`
represents one band in the dock. These represent all of the bands that an
extension would allow the user to add to their dock.
All of the `ICommandItem`s returned from `GetDockBands` **must** have a
`Command` with a non-empty `Id` set. If the `Id` is null or empty, DevPal will
ignore that band.
Bands are not automatically added to the dock. Instead, the user must choose
which bands they want to add. This is done via the DevPal settings page.
Furthermore, bands are not displayed in the list of commands in DevPal itself.
This allows extension authors to create objects that are only intended for the
dock, without cluttering up the main DevPal UI, and vice versa.
DevPal will then create UI in the dock for each band the user has chosen to add.
What that looks like will depend on the `Command` in the `ICommandItem`:
* A `IInvokableCommand` will be rendered as a single button. Think "the
time/date" button on the taskbar, that opens the notification center.
* A `IListPage` will be rendered as a strip of buttons, one for each `IListItem`
in the list. Think "media controls" for a music player.
* A `IContentPage` will be rendered as a single button. Clicking that button
will open a flyout with that content rendered in it. Think "weather" or "news"
flyouts.
If the `Command` in the `IListItem`s of a band are pages, then clicking those
buttons will open DevPal to that page, as if it were a flyout from the dock.
The `.Title` property of the top-level `ICommandItem` representing the band will
be used as the name of the band in the settings. So a media player band might
want to set the `Title` to "Contoso Music Player", even if the individual
buttons in the band don't show that title.
Users may also "pin" a top-level command from DevPal into the dock. DevPal will
take care of creating a new band (owned by devpal) with that command in it. This
allows users to add quick shortcuts to their favorite commands in the dock.
Think: pinning an app, or pinning a particular GitHub query.
Bands are added via ID. An extension may choose to have a TopLevelCommand and a
DockBand with the same `Id`. In this case, if the user pins the TopLevelCommand
to the dock, DevPal will pin the band from `GetDockBands`, rather than creating
a simple pinned command. This allows extension authors to seamlessly have a
top-level command present a palette-specific experience, while also having a
dock-specific experience. In our ongoing media player example, the top-level
command might open DevPal to a full-featured music control page, while the dock
band has simpler buttons on it (without a title/subtitle).
Users may choose to have:
* the orientation of the dock: vertical or horizontal
* the size of the dock
* which bands are shown in the dock
* whether the "labels" (read: `Title` & `Subtitle`) of individual bands are
shown or hidden.
- Dock bands will still display the `Title` & `Subtitle` of each item in the
band as the tooltip on those items, even when the "labels" are hidden.
## Class diagram
This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.)

View File

@@ -6,7 +6,10 @@ using Windows.Foundation;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2
public abstract partial class CommandProvider :
ICommandProvider,
ICommandProvider2,
ICommandProvider3
{
public virtual string Id { get; protected set; } = string.Empty;
@@ -48,6 +51,21 @@ public abstract partial class CommandProvider : ICommandProvider, ICommandProvid
}
}
/// <summary>
/// Get the dock bands provided by this command provider. Dock bands are
/// strips of items that appear on various UI surfaces in CmdPal, such as a
/// toolbar. Each ICommandItem returned from this method will be treated as
/// one atomic band by cmdpal.
///
/// If the command on an item here is a
/// IListPage, then cmdpal will render all of the items on that page as one
/// band. You can use this to create complex bands with multiple buttons.
/// </summary>
public virtual ICommandItem[]? GetDockBands()
{
return null;
}
/// <summary>
/// This is used to manually populate the WinRT type cache in CmdPal with
/// any interfaces that might not follow a straight linear path of requires.

View File

@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CommandPalette.Extensions.Toolkit;
/// <summary>
/// Helper class for creating a band out of a set of items. This allows you to
/// simply just instantiate a set of buttons as ListItems, then pass them in to
/// this class to create a band from those items. For example:
///
/// ```cs
/// var foo = new MyFooListItem();
/// var bar = new MyBarListItem();
/// var band = new WrappedDockItem([foo, bar], "com.me.myBand", "My cool desk band");
/// ```
/// </summary>
public partial class WrappedDockItem : CommandItem
{
public override string Title => _itemTitle;
public override IIconInfo? Icon
{
get => _icon; set { _icon = value; }
}
public override ICommand? Command => _backingList;
private readonly string _itemTitle;
private readonly WrappedDockList _backingList;
private IIconInfo? _icon;
public IListItem[] Items { get => _backingList.GetItems(); set => _backingList.SetItems(value); }
public WrappedDockItem(
ICommand command,
string displayTitle)
{
_backingList = new WrappedDockList(command);
_itemTitle = string.IsNullOrEmpty(displayTitle) ? command.Name : displayTitle;
_icon = command.Icon;
}
// This was too much of a foot gun - we'd internally create a ListItem that
// didn't bubble the prop change events back up. That was bad.
// public WrappedDockItem(
// ICommandItem item,
// string id,
// string displayTitle)
// {
// _backingList = new WrappedDockList(item, id);
// _itemTitle = string.IsNullOrEmpty(displayTitle) ? item.Title : displayTitle;
// _icon = item.Icon;
// }
/// <summary>
/// Initializes a new instance of the <see cref="WrappedDockItem"/> class.
/// Create a new dock band for a set of list items
/// </summary>
public WrappedDockItem(
IListItem[] items,
string id,
string displayTitle)
{
_backingList = new WrappedDockList(items, id, displayTitle);
_itemTitle = displayTitle;
}
}

View File

@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CommandPalette.Extensions.Toolkit;
/// <summary>
/// Helper class for a list page that just holds a set of items as a band.
/// The page itself doesn't do anything interesting.
/// </summary>
internal sealed partial class WrappedDockList : ListPage
{
private string _id;
public override string Id => _id;
private List<IListItem> _items;
internal WrappedDockList(ICommand command)
{
_items = new() { new ListItem(command) };
Name = command.Name;
_id = command.Id;
}
// Maybe revisit sometime.
// The hard problem is that the wrapping item will not
// listen for property changes on the inner item.
// public WrappedDockList(ICommandItem item, string id)
// {
// var command = item.Command;
// _items = new()
// {
// new ListItem(command)
// {
// Title = item.Title,
// Subtitle = item.Subtitle,
// Icon = item.Icon,
// MoreCommands = item.MoreCommands,
// },
// };
// Name = command.Name;
// _id = string.IsNullOrEmpty(id) ? command.Id : id;
// }
/// <summary>
/// Initializes a new instance of the <see cref="WrappedDockList"/> class.
/// Create a new list page for the set of items provided.
/// </summary>
internal WrappedDockList(IListItem[] items, string id, string name)
{
_items = new(items);
Name = name;
_id = id;
}
internal WrappedDockList(ICommand[] items, string id, string name)
{
_items = new();
foreach (var item in items)
{
_items.Add(new ListItem(item));
}
Name = name;
_id = id;
}
public override IListItem[] GetItems()
{
return _items.ToArray();
}
internal void SetItems(IListItem[]? newItems)
{
if (newItems == null)
{
_items = [];
RaiseItemsChanged(0);
return;
}
ListHelpers.InPlaceUpdateList(_items, newItems);
RaiseItemsChanged(_items.Count);
}
}

View File

@@ -405,6 +405,11 @@ namespace Microsoft.CommandPalette.Extensions
{
Object[] GetApiExtensionStubs();
};
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface ICommandProvider3 requires ICommandProvider2
{
ICommandItem[] GetDockBands();
};
}

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

@@ -89,6 +89,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public StringProperty RecordFormat { get; set; }
public BoolProperty CaptureSystemAudio { get; set; }
public BoolProperty CaptureAudio { get; set; }
public StringProperty MicrophoneDeviceId { 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

@@ -285,6 +285,9 @@
<ComboBoxItem>MP4</ComboBoxItem>
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="ZoomItRecordCaptureSystemAudio" ContentAlignment="Left">
<CheckBox x:Uid="ZoomIt_Record_CaptureSystemAudio" IsChecked="{x:Bind ViewModel.RecordCaptureSystemAudio, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="ZoomItRecordCaptureAudio" ContentAlignment="Left">
<CheckBox x:Uid="ZoomIt_Record_CaptureAudio" IsChecked="{x:Bind ViewModel.RecordCaptureAudio, Mode=TwoWay}" />
</tkcontrols:SettingsCard>

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@@ -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>
@@ -4834,9 +4837,9 @@ Activate by holding the key for the character you want to add an accent to, then
<value>Zoom in or out to enlarge content and make details clearer.</value>
</data>
<data name="ZoomIt_ZoomFAQ.Text" xml:space="preserve">
<value>Press **the mouse wheel** or **the Up / Down arrow keys** to zoom in or out.
Press **Esc** or **the right mouse button** to exit zoom mode.
Press **Ctrl + C** to capture the zoomed view, or **Ctrl + S** to save it.
<value>Press **the mouse wheel** or **the Up / Down arrow keys** to zoom in or out.
Press **Esc** or **the right mouse button** to exit zoom mode.
Press **Ctrl + C** to capture the zoomed view, or **Ctrl + S** to save it.
Press **Ctrl + Shift** to crop before copying or saving.</value>
</data>
<data name="ZoomIt_Zoom_Shortcut.Header" xml:space="preserve">
@@ -4864,23 +4867,23 @@ Press **Ctrl + Shift** to crop before copying or saving.</value>
<value>Draw</value>
</data>
<data name="ZoomIt_DrawFAQ.Text" xml:space="preserve">
<value>Press **the left mouse button** to toggle drawing mode when zoomed in, and **the right mouse button** to exit.
<value>Press **the left mouse button** to toggle drawing mode when zoomed in, and **the right mouse button** to exit.
Press **Ctrl + Z** to undo, **E** to clear drawings, and **Space** to center the cursor.
**Pen control**
**Pen control**
Press **Ctrl + the mouse wheel** or **Ctrl + Up / Down** to adjust the pen width.
**Colors**
**Colors**
Press **R** (Red), **G** (Green), **B** (Blue), **O** (Orange), **Y** (Yellow), or **P** (Pink) to switch colors.
**Highlight and blur**
**Highlight and blur**
Press **Shift + a color key** for a translucent highlighter, **X** for blur, or **Shift + X** for a stronger blur.
**Shapes**
**Shapes**
Press **Shift** for a line, **Ctrl** for a rectangle, **Tab** for an ellipse, or **Shift + Ctrl** for an arrow.
**Screen**
Press **W** or **K** for a white or black sketch pad.
**Screen**
Press **W** or **K** for a white or black sketch pad.
Press **Ctrl + C** to copy or **Ctrl + S** to save, and **Ctrl + Shift** to crop.
</value>
</data>
@@ -4907,16 +4910,16 @@ Press **Ctrl + C** to copy or **Ctrl + S** to save, and **Ctrl + Shift** to crop
<value>Insert predefined text snippets with a shortcut using a text file.</value>
</data>
<data name="ZoomIt_DemoTypeFAQ" xml:space="preserve">
<value>Text can be pulled from the clipboard when it starts with **[start]**.
Use **[end]** to separate snippets, **[pause:n]** to insert pauses (in seconds), and **[paste]** / **[/paste]** to send clipboard text.
<value>Text can be pulled from the clipboard when it starts with **[start]**.
Use **[end]** to separate snippets, **[pause:n]** to insert pauses (in seconds), and **[paste]** / **[/paste]** to send clipboard text.
Use **[enter]**, **[up]**, **[down]**, **[left]**, and **[right]** to issue keystrokes.
ZoomIt can send text automatically or run in manual mode. Keyboard input is blocked while text is being sent.
In manual mode, press **Space** to unblock keyboard input at the end of a snippet.
In manual mode, press **Space** to unblock keyboard input at the end of a snippet.
In auto mode, control returns automatically after completion.
At the end of the file, ZoomIt reloads the file and restarts from the beginning.
At the end of the file, ZoomIt reloads the file and restarts from the beginning.
Press the hotkey with **Shift** in the opposite mode to step back to the previous **[end]** marker.
Press **{0}** to reset DemoType and start from the beginning.</value>
@@ -4952,12 +4955,12 @@ Press **{0}** to reset DemoType and start from the beginning.</value>
<value>Displays a countdown overlay for timed breaks or presentations.</value>
</data>
<data name="ZoomIt_BreakFAQ.Text" xml:space="preserve">
<value>Enter timer mode from the ZoomIt tray icons Break menu.
<value>Enter timer mode from the ZoomIt tray icons Break menu.
Press **the arrow keys** to adjust the time. If the timer window loses focus through **Alt + Tab**, press **the left mouse button** on the ZoomIt tray icon to reactivate it.
Press **Esc** to exit timer mode.
Change the break timer color using the same keys as the drawing colors.
Change the break timer color using the same keys as the drawing colors.
The break timer font matches the text font.</value>
</data>
<data name="ZoomIt_Break_Shortcut.Header" xml:space="preserve">
@@ -5098,6 +5101,9 @@ The break timer font matches the text font.</value>
<data name="ZoomIt_Record_Format.Header" xml:space="preserve">
<value>Format</value>
</data>
<data name="ZoomIt_Record_CaptureSystemAudio.Content" xml:space="preserve">
<value>Capture system audio</value>
</data>
<data name="ZoomIt_Record_CaptureAudio.Content" xml:space="preserve">
<value>Capture audio input</value>
</data>
@@ -5992,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>
@@ -6167,14 +6236,14 @@ The break timer font matches the text font.</value>
</data>
<data name="ZoomIt_LiveZoom_Shortcut_Draw" xml:space="preserve">
<value>Press **{0}** to activate live drawing and **Esc** to clear annotations or to exit.
Press **Ctrl + Up / Down** to adjust the zoom level.
</value>
</data>
<data name="ZoomIt_TypeFAQ.Text" xml:space="preserve">
<value>Press **T** to switch to typing when drawing mode is active, and **Shift** for right-aligned text.
Press **Esc** or **the left mouse button** to exit typing mode.
Press **the mouse wheel** or **Up / Down** to adjust the font size.
<value>Press **T** to switch to typing when drawing mode is active, and **Shift** for right-aligned text.
Press **Esc** or **the left mouse button** to exit typing mode.
Press **the mouse wheel** or **Up / Down** to adjust the font size.
Text uses the current drawing color.</value>
</data>
<data name="ZoomIt_DrawGroup.Description" xml:space="preserve">

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

View File

@@ -850,6 +850,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool RecordCaptureSystemAudio
{
get => _zoomItSettings.Properties.CaptureSystemAudio.Value;
set
{
if (_zoomItSettings.Properties.CaptureSystemAudio.Value != value)
{
_zoomItSettings.Properties.CaptureSystemAudio.Value = value;
OnPropertyChanged(nameof(RecordCaptureSystemAudio));
NotifySettingsChanged();
}
}
}
public bool RecordCaptureAudio
{
get => _zoomItSettings.Properties.CaptureAudio.Value;