Compare commits

...

10 Commits

Author SHA1 Message Date
Yexuan Xiao
19c9b4e1fd [Light Switch] Switch desktop wallpapers with the Light/Dark mode (#42624)
I'm not sure how to set the AccentColor and ColorizationColor that is
consistent with the Settings App, and the same goes for switching
themes. If anyone has a solution, please let me know.


<img width="2010" height="1274" alt="2025-10-26 235808"
src="https://github.com/user-attachments/assets/b3eda45a-09f3-43bc-b87c-1b05bc308c24"
/>


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

---------

Co-authored-by: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com>
2026-01-06 12:55:55 -05:00
fm-sys
d9709b2b91 Add non-updating mode for Crop-And-Lock (#40720)
<!-- 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

Adds a "screenshot" mode to Crop And Lock, which allows creating a
window showing a freezed snapshot of the original window.


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

- [x] **Closes:** #31799, #33071 (also requested in the already closed
duplicate issues #28633, #33812, #37337, )
- [ ] **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 (crop-and-lock utility
doesn't have any tests)
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** Added/updated
- [x] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [x] **Documentation updated:**
https://github.com/MicrosoftDocs/windows-dev-docs/pull/5528

<!-- 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 was asked why this feature is needed at all, because it could be done
with snipping tool and just AoT that window as well. While this is true,
PowerToys goal always was to improve and speed up workflows. Instead of
capturing the screenshot, opening it, and then apply "Crop and Lock" or
"Always on Top" on the screenshots window, this PR aims to provide this
functionality in a single step.

Example use cases:
- _when I want to compare between two situations like previous output
result and current output result._ (#31799)
- _Allow cropping a section of a large code file (say top while working
at the bottom) as reference while working elsewhere in the file._
(#33071)
- _Can be useful for the work in the same document, like excel or word
where you are actively checking the data from the same document._
(#28633)
- _In lot's of older applications, if you need to get some information
or data from one dialog do another, but because of dialog modality it's
not possible to have both windows open at the same time._ (#33812)
- _nowadays quite a lot is happening inside the browser. Quite often, I
want to keep a small portion of the current website visible and switch
to e.g. the writing tool also running in a different tab in the same
browser window._ (#31799)


I've used win+ctrl+shift+s as the default activation shortcut, as it's
not yet used by other powertoys utilities, has similarity with the
normal win+shift+s shortcut hotkey and is consistent with the other Crop
and Lock shortcuts win+ctrl+shift+r (Reparent Mode) and win+ctrl+shift+t
(Thumbnail Mode).

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

Compatibility tested manually with a large set of applications I have
installed on my computer. However, automated tests don't really make
sense as there is not much business logic which could be tested.

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: vanzue <vanzue@outlook.com>
2026-01-06 21:08:17 +08:00
Shawn Yuan
03aec1a9a7 [File Locksmith] Add CLI support (#44047)
<!-- 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 introduces a new command-line interface (CLI) project
for the File Locksmith module, enabling users to interact with File
Locksmith functionality directly from the command line. The changes
include project and build configuration, CLI implementation, and
supporting code to integrate with the existing FileLocksmith library.

## Commands and Options

| Command / Option | Alias | Description |
| :--- | :--- | :--- |
| `<path>` | N/A | **Required**. One or more file or directory paths to
check. You can specify multiple paths separated by spaces. |
| `--kill` | N/A | Terminates (kills) all processes that are currently
locking the specified files. |
| `--json` | N/A | Outputs the results in structured **JSON** format
instead of human-readable text. Useful for automation and scripts. |
| `--wait` | N/A | **Blocks execution** and waits until the specified
files are released. The command will not exit until the files are
unlocked. |
| `--help` | N/A | Displays the help message with usage instructions. |

## Usage Examples

### 1. Basic check (Human-readable output)
Check which processes are locking a specific file:
```powershell
FileLocksmithCLI.exe "C:\Users\Docs\report.docx"
```

### 2. Check multiple files and output JSON
Check multiple files and get the output in JSON format for parsing:
```powershell
FileLocksmithCLI.exe --json "C:\File1.txt" "C:\Folder\File2.dll"
```

### 3. Wait for a file to be unlocked
Block script execution until a file is released (useful in build
scripts):
```powershell
FileLocksmithCLI.exe --wait "C:\bin\output.exe"
```

### 4. Force unlock a file
Kill all processes that are locking a specific file:
```powershell
FileLocksmithCLI.exe --kill "C:\LockedFile.dat"
```

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

---------

Signed-off-by: Shawn Yuan (from Dev Box) <shuaiyuan@microsoft.com>
2026-01-06 15:59:37 +08:00
Jaylyn Barbee
335dcbaae8 [Light Switch] Adding telemetry events (#44241)
Adding events to track the following:
<table style="width:100%">
  <tr>
    <th>Event Name</th>
    <th>Description</th>
<th>Data collected</th>
  </tr>
  <tr>
    <td>Microsoft.PowerToys.LightSwitch_EnableLightSwitch</td>
    <td>Triggered when Light Switch is enabled or disabled.</td>
<td>Whether the module is enabled or disabled (bool)</td>
  </tr>
  <tr>
    <td>Microsoft.PowerToys.LightSwitch_ShortcutInvoked</td>
    <td>Occurs when the shortcut for Light Switch is invoked.</td>
<td></td>
  </tr>
  <tr>
    <td>Microsoft.PowerToys.LightSwitch_ScheduleModeToggled</td>
<td>Occurs when a new schedule mode is selected for Light Switch.</td>
<td>The new mode selected (string)</td>
  </tr>
  <tr>
    <td>Microsoft.PowerToys.LightSwitch_ThemeTargetChanged</td>
<td>Occurs when the options for targeting the system or apps is
updated.</td>
<td>The new options selected (two bools)</td>
  </tr>
</table>

The above events that are related to Light Switch settings are tracked
in the "LoadSettings" function inside the service but only if the value
has changed. The Enabled event as well as the Shortcut event are tracked
in the module interface.
2026-01-06 09:54:37 +08:00
Ruben Fricke
6515985823 CmdPal: Update Extension SDK docs link to Microsoft Learn (#44517)
<!-- 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
Update CmdPal SDK docs links to point to Microsoft Learn as requested in
#40994.

 

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

  - [x] Closes: #40994
<!-- - [ ] 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
  - [x] **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
  Updated links in:
    - src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml
    - src/modules/cmdpal/extensionsdk/README.md

Note: The issue proposes linking directly to “Command Palette /
Developer Docs / Extensibility overview” on
Learn. I noticed the project often uses fwlink/aka.ms redirects instead
of direct Learn URLs. If creating a
redirect is preferred (or if I can create one), let me know and I’ll
update the links accordingly.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation
  steps taken as well -->
  ## Validation Steps Performed
Manually verified that the links resolve to the Microsoft Learn Command
Palette extensibility overview.

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-01-05 18:43:16 -06:00
Kai Tao
36a64cae90 Find My Mouse: Improve spotlight edge quality (#44533)
<!-- 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

Find my mouse Optimization:
Crisp, soft edge that doesn't scale with radius—avoids overly blurry
edges at large sizes or hard edges at small sizes

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

| Optimization | Before | After |
|--------|--------|--------|
| Large Radius |<img width="380" height="257" alt="image"
src="https://github.com/user-attachments/assets/66d3e180-71db-42aa-a003-8178bc9bee0d"
/>|<img width="446" height="420" alt="image"
src="https://github.com/user-attachments/assets/c2bd45ef-2d1d-485c-8669-d478b2bb9ec7"
/>|
|--------|--------|--------|
| Small Radius |<img width="217" height="222" alt="image"
src="https://github.com/user-attachments/assets/c19019fb-1671-460f-b1e5-37eb30e72c9e"
/>|<img width="135" height="123" alt="image"
src="https://github.com/user-attachments/assets/6c6f721b-6b2d-4631-af4c-d03c517a260d"
/>|
2026-01-05 22:41:01 +08:00
NOXX - Commiter
30f832ac65 Fix typo in 'Open With Antigravity' plugin name (#44529)
Fix typo in 'Antygravity' to 'Antigravity' and add Project Launcher
Plugin

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

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-01-05 12:27:40 +01:00
Shawn Yuan
e0c0e7bb76 Fixed keyvisual issue for pt run (#41539)
<!-- 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: #41468 
- [ ] **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

---------

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2026-01-05 13:38:43 +08:00
Michael Clayton
dfede67993 Ready for Review - [Mouse Without Borders] - refactoring "Common" classes (Part 7 of 7) (#44283)
## Summary of the Pull Request

**Part 7** (last part) of a [slow-running 7-part
refactor](https://github.com/microsoft/PowerToys/issues/35155#issuecomment-2583334110)
of the giant "Common" class in Mouse Without Borders into individual
classes with tighter private scope.

In this PR:

* Extract the "Common" code from the following files:
  * ```Common.cs```-> ```Core/Common.cs```
  * ```IClipboardHelper.cs/Common``` -> ```Core/IpcChannelHelper.cs```
* Update references to the types in the new locations
* Update unit test to verify functionality has only changed in an
expected way
* Make members private or internal as applicable

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

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

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

### Run manual tests from [Test Checklist
Template](5bc7201ae2/doc/releases/tests-checklist-template.md (mouse-without-borders)):

* Install PowerToys on two PCs in the same local network:
   - [x]     Verify that PowerToys is properly installed on both PCs.
   - [x]     Configure Windows Firewall Rules
- ```netsh advfirewall firewall add rule
name="PowerToys.MouseWithoutBorders - mc" dir=in action=allow
program="C:\src\mc\PowerToys\x64\Debug\PowerToys.exe" enable=yes
remoteip=any profile=any protocol=tcp```
   
 * Setup Connection:
- [x] Open MWB's settings on the first PC and click the "New Key"
button. Verify that a new security key is generated.
- [x] Copy the generated security key and paste it in the corresponding
input field in the settings of MWB on the second PC. Also enter the name
of the first PC in the required field.
- [x] Press "Connect" and verify that the machine layout now includes
two PC tiles, each displaying their respective PC names.
   
 * Verify Connection Status:
- [x] Ensure that the border of the remote PC turns green, indicating a
successful connection.
- [x] Enter an incorrect security key and verify that the border of the
remote PC turns red, indicating a failed connection.
   
 * Test Remote Mouse/Keyboard Control:
- [x] With the PCs connected, test the mouse/keyboard control from one
PC to another. Verify that the mouse/keyboard inputs are correctly
registered on the other PC.
- [ ] Test remote mouse/keyboard control across all four PCs, if
available. Verify that inputs are correctly registered on each connected
PC when the mouse is active there.
     - unable to test - only 2 machines available
   
 * Test Remote Control with Elevated Apps:
- note - the main PowerToys.exe must be running as a **non**-admin for
these tests
- [x] Open an elevated app on one of the PCs. Verify that without "Use
Service" enabled, PowerToys does not control the elevated app.
- [x] Enable "Use Service" in MWB's settings (need to run PowerToys.exe
as admin to enable "Use Service", then restart PowerToys.exe as
non-admin). Verify that PowerToys can now control the elevated app
remotely. Verify that MWB processes are running as LocalSystem, while
the MWB helper process is running non-elevated.
- ```get-process -Name "PowerToys.MouseWithoutBorders*" -IncludeUserName
| format-table Id, ProcessName, UserName```
- [x] Process: ```PowerToys.MouseWithoutBorders.exe``` - running as
```SYSTEM```
- [x] Process: ```PowerToys.MouseWithoutBorders.Helper.exe``` - running
as current user
- ```get-service -Name "PowerToys.*" | ft Status, Name, UserName;
get-ciminstance -Class "Win32_Service" -Filter "Name like 'PowerToys%'"
| ft ProcessId, Name```
- [x] Service: ```PowerToys.MWB.Service``` - running as ```Local
System```
- [x] Toggle "Use Service" again, verify that each time you do that, the
MWB processes are restarted.
- [x] Run PowerToys elevated on one of the machines, verify that you can
control elevated apps remotely now on that machine.

* Test Module Enable Status:
- [x] For all combinations of "Use Service"/"Run PowerToys as admin",
try enabling/disabling MWB module and verify that it's indeed being
toggled using task manager.
   
 * Test Disconnection/Reconnection:
- [x] Disconnect one of the PCs from network. Verify that the machine
layout updates to reflect the disconnection.
   - [x]     Do the same, but now by exiting PowerToys.
   - [x]     Start PowerToys again, verify that the PCs are reconnected.
  
 * Test Various Local Network Conditions:
- [ ] Test MWB performance under various network conditions (e.g., low
bandwidth, high latency). Verify that the tool maintains a stable
connection and functions correctly.

 * Clipboard Sharing:
- [x] Copy some text on one PC and verify that the same text can be
pasted on another PC.
- [x] Use the screenshot key and Win+Shift+S to take a screenshot on one
PC and verify that the screenshot can be pasted on another PC.
- [x] Copy a file in Windows Explorer and verify that the file can be
pasted on another PC. Make sure the file size is below 100MB.
- [x] Try to copy multiple files and directories and verify that it's
not possible (only the first selected file is being copied).
 
 * Drag and Drop:
- [ ] Drag a file from Windows Explorer on one PC, cross the screen
border onto another PC, and release it there. Verify that the file is
copied to the other PC. Make sure the file size is below 100MB.
- [ ] While dragging the file, verify that a corresponding icon is
displayed under the mouse cursor.
- [ ] Without moving the mouse from one PC to the target PC, press
CTRL+ALT+F1/2/3/4 hotkey to switch to the target PC directly and verify
that file sharing/dropping is not working.

 * Lock and Unlock with "Use Service" Enabled:
   - [x]     Enable "Use Service" in MWB's settings.
- [x] Lock a remote PC using Win+L, move the mouse to it remotely, and
try to unlock it. Verify that you can unlock the remote PC.
- [x] Disable "Use Service" in MWB's settings, lock the remote PC, move
the mouse to it remotely, and try to unlock it. Verify that you can't
unlock the remote PC.

 * Test Settings:
- [ ] Change the rest of available settings on MWB page and verify that
each setting works as described.

### Group Policy Tests

See https://learn.microsoft.com/en-us/windows/powertoys/grouppolicy

- [ ] Install *.admx / *.adml and check settings behave as expected
  - [ ] I'll expand the list of settings here when I get this far :-)
- [ ] HKEY_LOCAL_MACHINE\SOFTWARE\Policies\PowerToys
  - [x]     ConfigureEnabledUtilityMouseWithoutBorders
- [x] ```[missing]``` - "Activation -> Enable Mouse Without Borders"
enabled, with GPO warning hidden
- ```reg delete HKEY_LOCAL_MACHINE\SOFTWARE\Policies\PowerToys /v
ConfigureEnabledUtilityMouseWithoutBorders /f```
- [x] ```0``` - "Activation -> Enable Mouse Without Borders" set to
"off" and disabled, with GPO warning visible
- ```reg add HKEY_LOCAL_MACHINE\SOFTWARE\Policies\PowerToys /v
ConfigureEnabledUtilityMouseWithoutBorders /t REG_DWORD /d 0 /f```
- [x] ```1``` - "Activation -> Enable Mouse Without Borders" set to "on"
and disabled, with GPO warning visible
- ```reg add HKEY_LOCAL_MACHINE\SOFTWARE\Policies\PowerToys /v
ConfigureEnabledUtilityMouseWithoutBorders /t REG_DWORD /d 1 /f```
  - [ ] MwbClipboardSharingEnabled
  - [ ] MwbFileTransferEnabled
  - [ ] MwbUseOriginalUserInterface
  - [ ] MwbDisallowBlockingScreensaver
  - [ ] MwbSameSubnetOnly
  - [ ] MwbValidateRemoteIp
  - [x]     MwbDisableUserDefinedIpMappingRules
- [x] ```[missing]``` - "Advanced Settings -> IP address mapping"
enabled, with GPO warning hidden
- ```reg delete HKEY_LOCAL_MACHINE\SOFTWARE\Policies\PowerToys /v
MwbDisableUserDefinedIpMappingRules /f```
- [x] ```0``` - "Advanced Settings -> IP address mapping" enabled, with
GPO warning hidden
- ```reg add HKEY_LOCAL_MACHINE\SOFTWARE\Policies\PowerToys /v
MwbDisableUserDefinedIpMappingRules /t REG_DWORD /d 0 /f```
- [x] ```1``` - "Advanced Settings -> IP address mapping" disabled, with
GPO warning visible
- ```reg add HKEY_LOCAL_MACHINE\SOFTWARE\Policies\PowerToys /v
MwbDisableUserDefinedIpMappingRules /t REG_DWORD /d 1 /f```
  - [x]     MwbPolicyDefinedIpMappingRules
- [x] ```[missing]``` - "Advanced Settings -> IP address mapping"
enabled, with GPO warning and GPO values hidden
- ```reg delete HKEY_LOCAL_MACHINE\SOFTWARE\Policies\PowerToys /v
MwbPolicyDefinedIpMappingRules /f```
- [x] ```[empty value]``` - "Advanced Settings -> IP address mapping"
enabled, with GPO warning hidden and GPO values hidden
- ```reg add HKEY_LOCAL_MACHINE\SOFTWARE\Policies\PowerToys /v
MwbPolicyDefinedIpMappingRules /t REG_MULTI_SZ /d "" /f```
- [x] ```[non-empty value]``` - "Advanced Settings -> IP address
mapping" enabled, with GPO warning visible and GPO values visible
- ```reg add HKEY_LOCAL_MACHINE\SOFTWARE\Policies\PowerToys /v
MwbPolicyDefinedIpMappingRules /t REG_MULTI_SZ /d "aaa 10.0.0.1\0bbb
10.0.0.2" /f```
2026-01-05 12:02:12 +08:00
Kai Tao
1438a628ad Find My Mouse: Add telemetry for trigger source (#44446)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Verified trace successfully locally
2026-01-05 11:12:25 +08:00
106 changed files with 4447 additions and 2372 deletions

View File

@@ -456,6 +456,7 @@ DWMWINDOWATTRIBUTE
DWMWINDOWMAXIMIZEDCHANGE DWMWINDOWMAXIMIZEDCHANGE
DWORDLONG DWORDLONG
dworigin dworigin
DWPOS
dwrite dwrite
dxgi dxgi
eab eab
@@ -676,6 +677,7 @@ hicon
HICONSM HICONSM
HIDEREADONLY HIDEREADONLY
HIDEWINDOW HIDEWINDOW
hif
Hif Hif
HIMAGELIST HIMAGELIST
himl himl
@@ -837,6 +839,7 @@ jpe
jpnime jpnime
Jsons Jsons
jsonval jsonval
jxl
jxr jxr
keybd keybd
KEYBDDATA KEYBDDATA
@@ -1460,6 +1463,7 @@ recyclebin
Redist Redist
Reencode Reencode
REFCLSID REFCLSID
REFGUID
REFIID REFIID
REGCLS REGCLS
regfile regfile
@@ -1480,6 +1484,7 @@ remoteip
Removelnk Removelnk
renamable renamable
RENAMEONCOLLISION RENAMEONCOLLISION
RENDERFULLCONTENT
reparented reparented
reparenting reparenting
reportfileaccesses reportfileaccesses
@@ -1565,6 +1570,7 @@ SETBUDDYINT
SETCONTEXT SETCONTEXT
SETCURSEL SETCURSEL
setcursor setcursor
SETDESKWALLPAPER
SETFOCUS SETFOCUS
SETFOREGROUND SETFOREGROUND
SETHOTKEY SETHOTKEY
@@ -1653,6 +1659,7 @@ SKEXP
SKIPOWNPROCESS SKIPOWNPROCESS
sku sku
SLGP SLGP
slideshow
sln sln
slnf slnf
slnx slnx
@@ -1810,7 +1817,6 @@ TILEDWINDOW
TILLSON TILLSON
timedate timedate
timediff timediff
timeunion
timeutil timeutil
TITLEBARINFO TITLEBARINFO
Titlecase Titlecase
@@ -1890,6 +1896,7 @@ unwide
unzoom unzoom
UOffset UOffset
UOI UOI
UPDATEINIFILE
UPDATENOW UPDATENOW
UPDATEREGISTRY UPDATEREGISTRY
updown updown

View File

@@ -117,6 +117,7 @@
"WinUI3Apps\\PowerToys.FileLocksmithUI.dll", "WinUI3Apps\\PowerToys.FileLocksmithUI.dll",
"WinUI3Apps\\PowerToys.FileLocksmithContextMenu.dll", "WinUI3Apps\\PowerToys.FileLocksmithContextMenu.dll",
"FileLocksmithContextMenuPackage.msix", "FileLocksmithContextMenuPackage.msix",
"FileLocksmithCLI.exe",
"WinUI3Apps\\Peek.Common.dll", "WinUI3Apps\\Peek.Common.dll",
"WinUI3Apps\\Peek.FilePreviewer.dll", "WinUI3Apps\\Peek.FilePreviewer.dll",

View File

@@ -694,6 +694,30 @@ _If you want to find diagnostic data events in the source code, these two links
</tr> </tr>
</table> </table>
### Light Switch
<table style="width:100%">
<tr>
<th>Event Name</th>
<th>Description</th>
</tr>
<tr>
<td>Microsoft.PowerToys.LightSwitch_EnableLightSwitch</td>
<td>Triggered when Light Switch is enabled or disabled.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.LightSwitch_ShortcutInvoked</td>
<td>Occurs when the shortcut for Light Switch is invoked.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.LightSwitch_ScheduleModeToggled</td>
<td>Occurs when a new schedule mode is selected for Light Switch.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.LightSwitch_ThemeTargetChanged</td>
<td>Occurs when the options for targeting the system or apps is updated.</td>
</tr>
</table>
### Mouse Highlighter ### Mouse Highlighter
<table style="width:100%"> <table style="width:100%">
<tr> <tr>

View File

@@ -420,6 +420,7 @@
</Project> </Project>
</Folder> </Folder>
<Folder Name="/modules/FileLocksmith/"> <Folder Name="/modules/FileLocksmith/">
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/FileLocksmithCLI.vcxproj" Id="49D456D3-F485-45AF-8875-45B44F193DDC" />
<Project Path="src/modules/FileLocksmith/FileLocksmithContextMenu/FileLocksmithContextMenu.vcxproj" Id="799a50d8-de89-4ed1-8ff8-ad5a9ed8c0ca" /> <Project Path="src/modules/FileLocksmith/FileLocksmithContextMenu/FileLocksmithContextMenu.vcxproj" Id="799a50d8-de89-4ed1-8ff8-ad5a9ed8c0ca" />
<Project Path="src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj" Id="57175ec7-92a5-4c1e-8244-e3fbca2a81de" /> <Project Path="src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj" Id="57175ec7-92a5-4c1e-8244-e3fbca2a81de" />
<Project Path="src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj" Id="9d52fd25-ef90-4f9a-a015-91efc5daf54f" /> <Project Path="src/modules/FileLocksmith/FileLocksmithLib/FileLocksmithLib.vcxproj" Id="9d52fd25-ef90-4f9a-a015-91efc5daf54f" />
@@ -429,6 +430,9 @@
<Platform Solution="*|x64" Project="x64" /> <Platform Solution="*|x64" Project="x64" />
</Project> </Project>
</Folder> </Folder>
<Folder Name="/modules/FileLocksmith/Tests/">
<Project Path="src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLIUnitTests.vcxproj" Id="A1B2C3D4-E5F6-7890-1234-567890ABCDEF" />
</Folder>
<Folder Name="/modules/Hosts/"> <Folder Name="/modules/Hosts/">
<Project Path="src/modules/Hosts/Hosts/Hosts.csproj"> <Project Path="src/modules/Hosts/Hosts/Hosts.csproj">
<Platform Solution="*|ARM64" Project="ARM64" /> <Platform Solution="*|ARM64" Project="ARM64" />

View File

@@ -20,6 +20,9 @@ Creates a window showing the selected area of the original window. Changes in th
### Reparent Mode ### Reparent Mode
Creates a window that replaces the original window, showing only the selected area. The application is controlled through the cropped window. Creates a window that replaces the original window, showing only the selected area. The application is controlled through the cropped window.
### Screenshot Mode
Creates a window showing a freezed snapshot of the original window.
## Code Structure ## Code Structure
### Project Layout ### Project Layout
@@ -30,6 +33,7 @@ The Crop and Lock module is part of the PowerToys solution. All the logic-relate
- **OverlayWindow.cpp**: Thumbnail module type's window concrete implementation. - **OverlayWindow.cpp**: Thumbnail module type's window concrete implementation.
- **ReparentCropAndLockWindow.cpp**: Defines the UI for the reparent mode. - **ReparentCropAndLockWindow.cpp**: Defines the UI for the reparent mode.
- **ChildWindow.cpp**: Reparent module type's window concrete implementation. - **ChildWindow.cpp**: Reparent module type's window concrete implementation.
- **ScreenshotCropAndLockWindow.cpp**: Defines the UI for the screenshot mode.
## Known Issues ## Known Issues

View File

@@ -50,7 +50,8 @@ Contact the developers of a plugin directly for assistance with a specific plugi
| [Hotkeys](https://github.com/ruslanlap/PowerToysRun-Hotkeys) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run. | | [Hotkeys](https://github.com/ruslanlap/PowerToysRun-Hotkeys) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run. |
| [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. | | [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. |
| [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI | | [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI |
| [Open With Antygravity](https://github.com/artickc/PowerToys-Run-Antygravity) | [artickc](https://github.com/artickc) | Open Visual Studio, VS Code recents with Cursor AI | | [Open With Antigravity](https://github.com/artickc/PowerToys-Run-Antygravity) | [artickc](https://github.com/artickc) | Open Visual Studio, VS Code recents with Antigravity AI |
| [Project Launcher Plugin](https://github.com/artickc/ProjectLauncherPowerToysPlugin) | [artickc](https://github.com/artickc) | Access your projects using Project Launcher and PowerToys Run |
| [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. | | [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. |
| [QuickAI](https://github.com/ruslanlap/PowerToysRun-QuickAi) | [ruslanlap](https://github.com/ruslanlap) | AI-powered assistance with instant, smart responses from multiple providers (Groq, Together, Fireworks, OpenRouter, Cohere) | | [QuickAI](https://github.com/ruslanlap/PowerToysRun-QuickAi) | [ruslanlap](https://github.com/ruslanlap) | AI-powered assistance with instant, smart responses from multiple providers (Groq, Together, Fireworks, OpenRouter, Cohere) |

View File

@@ -223,6 +223,10 @@ namespace winrt::PowerToys::Interop::implementation
{ {
return CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT; return CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT;
} }
hstring Constants::CropAndLockScreenshotEvent()
{
return CommonSharedConstants::CROP_AND_LOCK_SCREENSHOT_EVENT;
}
hstring Constants::ShowEnvironmentVariablesSharedEvent() hstring Constants::ShowEnvironmentVariablesSharedEvent()
{ {
return CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT; return CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT;

View File

@@ -59,6 +59,7 @@ namespace winrt::PowerToys::Interop::implementation
static hstring TerminateHostsSharedEvent(); static hstring TerminateHostsSharedEvent();
static hstring CropAndLockThumbnailEvent(); static hstring CropAndLockThumbnailEvent();
static hstring CropAndLockReparentEvent(); static hstring CropAndLockReparentEvent();
static hstring CropAndLockScreenshotEvent();
static hstring ShowEnvironmentVariablesSharedEvent(); static hstring ShowEnvironmentVariablesSharedEvent();
static hstring ShowEnvironmentVariablesAdminSharedEvent(); static hstring ShowEnvironmentVariablesAdminSharedEvent();
static hstring WorkspacesLaunchEditorEvent(); static hstring WorkspacesLaunchEditorEvent();

View File

@@ -56,6 +56,7 @@ namespace PowerToys
static String TerminateHostsSharedEvent(); static String TerminateHostsSharedEvent();
static String CropAndLockThumbnailEvent(); static String CropAndLockThumbnailEvent();
static String CropAndLockReparentEvent(); static String CropAndLockReparentEvent();
static String CropAndLockScreenshotEvent();
static String ShowEnvironmentVariablesSharedEvent(); static String ShowEnvironmentVariablesSharedEvent();
static String ShowEnvironmentVariablesAdminSharedEvent(); static String ShowEnvironmentVariablesAdminSharedEvent();
static String WorkspacesLaunchEditorEvent(); static String WorkspacesLaunchEditorEvent();

View File

@@ -132,6 +132,7 @@ namespace CommonSharedConstants
// Path to the events used by CropAndLock // Path to the events used by CropAndLock
const wchar_t CROP_AND_LOCK_REPARENT_EVENT[] = L"Local\\PowerToysCropAndLockReparentEvent-6060860a-76a1-44e8-8d0e-6355785e9c36"; const wchar_t CROP_AND_LOCK_REPARENT_EVENT[] = L"Local\\PowerToysCropAndLockReparentEvent-6060860a-76a1-44e8-8d0e-6355785e9c36";
const wchar_t CROP_AND_LOCK_THUMBNAIL_EVENT[] = L"Local\\PowerToysCropAndLockThumbnailEvent-1637be50-da72-46b2-9220-b32b206b2434"; const wchar_t CROP_AND_LOCK_THUMBNAIL_EVENT[] = L"Local\\PowerToysCropAndLockThumbnailEvent-1637be50-da72-46b2-9220-b32b206b2434";
const wchar_t CROP_AND_LOCK_SCREENSHOT_EVENT[] = L"Local\\PowerToysCropAndLockScreenshotEvent-ff077ab2-8360-4bd1-864a-637389d35593";
const wchar_t CROP_AND_LOCK_EXIT_EVENT[] = L"Local\\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a"; const wchar_t CROP_AND_LOCK_EXIT_EVENT[] = L"Local\\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a";
// Path to the events used by EnvironmentVariables // Path to the events used by EnvironmentVariables

View File

@@ -112,6 +112,7 @@
<ClCompile Include="ChildWindow.cpp" /> <ClCompile Include="ChildWindow.cpp" />
<ClCompile Include="main.cpp" /> <ClCompile Include="main.cpp" />
<ClCompile Include="ReparentCropAndLockWindow.cpp" /> <ClCompile Include="ReparentCropAndLockWindow.cpp" />
<ClCompile Include="ScreenshotCropAndLockWindow.cpp" />
<ClCompile Include="ThumbnailCropAndLockWindow.cpp" /> <ClCompile Include="ThumbnailCropAndLockWindow.cpp" />
<ClCompile Include="OverlayWindow.cpp" /> <ClCompile Include="OverlayWindow.cpp" />
<ClCompile Include="pch.cpp"> <ClCompile Include="pch.cpp">
@@ -126,6 +127,7 @@
<ClInclude Include="DisplaysUtil.h" /> <ClInclude Include="DisplaysUtil.h" />
<ClInclude Include="ModuleConstants.h" /> <ClInclude Include="ModuleConstants.h" />
<ClInclude Include="ReparentCropAndLockWindow.h" /> <ClInclude Include="ReparentCropAndLockWindow.h" />
<ClInclude Include="ScreenshotCropAndLockWindow.h" />
<ClInclude Include="ThumbnailCropAndLockWindow.h" /> <ClInclude Include="ThumbnailCropAndLockWindow.h" />
<ClInclude Include="SettingsWindow.h" /> <ClInclude Include="SettingsWindow.h" />
<ClInclude Include="OverlayWindow.h" /> <ClInclude Include="OverlayWindow.h" />

View File

@@ -12,6 +12,7 @@
<ClCompile Include="ReparentCropAndLockWindow.cpp" /> <ClCompile Include="ReparentCropAndLockWindow.cpp" />
<ClCompile Include="ChildWindow.cpp" /> <ClCompile Include="ChildWindow.cpp" />
<ClCompile Include="trace.cpp" /> <ClCompile Include="trace.cpp" />
<ClCompile Include="ScreenshotCropAndLockWindow.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
@@ -28,6 +29,7 @@
<ClInclude Include="trace.h" /> <ClInclude Include="trace.h" />
<ClInclude Include="ModuleConstants.h" /> <ClInclude Include="ModuleConstants.h" />
<ClInclude Include="DispatcherQueue.desktop.interop.h" /> <ClInclude Include="DispatcherQueue.desktop.interop.h" />
<ClInclude Include="ScreenshotCropAndLockWindow.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="CropAndLock.rc" /> <ResourceCompile Include="CropAndLock.rc" />

View File

@@ -0,0 +1,178 @@
#include "pch.h"
#include "ScreenshotCropAndLockWindow.h"
const std::wstring ScreenshotCropAndLockWindow::ClassName = L"CropAndLock.ScreenshotCropAndLockWindow";
std::once_flag ScreenshotCropAndLockWindowClassRegistration;
void ScreenshotCropAndLockWindow::RegisterWindowClass()
{
auto instance = winrt::check_pointer(GetModuleHandleW(nullptr));
WNDCLASSEXW wcex = {};
wcex.cbSize = sizeof(wcex);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.hInstance = instance;
wcex.hIcon = LoadIconW(instance, IDI_APPLICATION);
wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW);
wcex.hbrBackground = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
wcex.lpszClassName = ClassName.c_str();
wcex.hIconSm = LoadIconW(wcex.hInstance, IDI_APPLICATION);
winrt::check_bool(RegisterClassExW(&wcex));
}
ScreenshotCropAndLockWindow::ScreenshotCropAndLockWindow(std::wstring const& titleString, int width, int height)
{
auto instance = winrt::check_pointer(GetModuleHandleW(nullptr));
std::call_once(ScreenshotCropAndLockWindowClassRegistration, []() { RegisterWindowClass(); });
auto exStyle = 0;
auto style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN;
RECT rect = { 0, 0, width, height };
winrt::check_bool(AdjustWindowRectEx(&rect, style, false, exStyle));
auto adjustedWidth = rect.right - rect.left;
auto adjustedHeight = rect.bottom - rect.top;
winrt::check_bool(CreateWindowExW(exStyle, ClassName.c_str(), titleString.c_str(), style, CW_USEDEFAULT, CW_USEDEFAULT, adjustedWidth, adjustedHeight, nullptr, nullptr, instance, this));
WINRT_ASSERT(m_window);
}
ScreenshotCropAndLockWindow::~ScreenshotCropAndLockWindow()
{
DestroyWindow(m_window);
}
LRESULT ScreenshotCropAndLockWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam)
{
switch (message)
{
case WM_DESTROY:
if (m_closedCallback != nullptr && !m_destroyed)
{
m_destroyed = true;
m_closedCallback(m_window);
}
break;
case WM_PAINT:
if (m_captured && m_bitmap)
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(m_window, &ps);
HDC memDC = CreateCompatibleDC(hdc);
SelectObject(memDC, m_bitmap.get());
RECT clientRect = {};
GetClientRect(m_window, &clientRect);
int clientWidth = clientRect.right - clientRect.left;
int clientHeight = clientRect.bottom - clientRect.top;
int srcWidth = m_destRect.right - m_destRect.left;
int srcHeight = m_destRect.bottom - m_destRect.top;
float srcAspect = static_cast<float>(srcWidth) / srcHeight;
float dstAspect = static_cast<float>(clientWidth) / clientHeight;
int drawWidth = clientWidth;
int drawHeight = static_cast<int>(clientWidth / srcAspect);
if (dstAspect > srcAspect)
{
drawHeight = clientHeight;
drawWidth = static_cast<int>(clientHeight * srcAspect);
}
int offsetX = (clientWidth - drawWidth) / 2;
int offsetY = (clientHeight - drawHeight) / 2;
SetStretchBltMode(hdc, HALFTONE);
StretchBlt(hdc, offsetX, offsetY, drawWidth, drawHeight, memDC, 0, 0, srcWidth, srcHeight, SRCCOPY);
DeleteDC(memDC);
EndPaint(m_window, &ps);
}
break;
default:
return base_type::MessageHandler(message, wparam, lparam);
}
return 0;
}
void ScreenshotCropAndLockWindow::CropAndLock(HWND windowToCrop, RECT cropRect)
{
if (m_captured)
{
return;
}
// Get full window bounds
RECT windowRect{};
winrt::check_hresult(DwmGetWindowAttribute(
windowToCrop,
DWMWA_EXTENDED_FRAME_BOUNDS,
&windowRect,
sizeof(windowRect)));
RECT clientRect = ClientAreaInScreenSpace(windowToCrop);
auto offsetX = clientRect.left - windowRect.left;
auto offsetY = clientRect.top - windowRect.top;
m_sourceRect = {
cropRect.left + offsetX,
cropRect.top + offsetY,
cropRect.right + offsetX,
cropRect.bottom + offsetY
};
int fullWidth = windowRect.right - windowRect.left;
int fullHeight = windowRect.bottom - windowRect.top;
HDC fullDC = CreateCompatibleDC(nullptr);
HDC screenDC = GetDC(nullptr);
HBITMAP fullBitmap = CreateCompatibleBitmap(screenDC, fullWidth, fullHeight);
HGDIOBJ oldFullBitmap = SelectObject(fullDC, fullBitmap);
// Capture full window
winrt::check_bool(PrintWindow(windowToCrop, fullDC, PW_RENDERFULLCONTENT));
// Crop
int cropWidth = m_sourceRect.right - m_sourceRect.left;
int cropHeight = m_sourceRect.bottom - m_sourceRect.top;
HDC cropDC = CreateCompatibleDC(nullptr);
HBITMAP cropBitmap = CreateCompatibleBitmap(screenDC, cropWidth, cropHeight);
HGDIOBJ oldCropBitmap = SelectObject(cropDC, cropBitmap);
ReleaseDC(nullptr, screenDC);
BitBlt(
cropDC,
0,
0,
cropWidth,
cropHeight,
fullDC,
m_sourceRect.left,
m_sourceRect.top,
SRCCOPY);
SelectObject(fullDC, oldFullBitmap);
DeleteObject(fullBitmap);
DeleteDC(fullDC);
SelectObject(cropDC, oldCropBitmap);
DeleteDC(cropDC);
m_bitmap.reset(cropBitmap);
// Resize our window
RECT dest{ 0, 0, cropWidth, cropHeight };
LONG_PTR exStyle = GetWindowLongPtrW(m_window, GWL_EXSTYLE);
LONG_PTR style = GetWindowLongPtrW(m_window, GWL_STYLE);
winrt::check_bool(AdjustWindowRectEx(&dest, static_cast<DWORD>(style), FALSE, static_cast<DWORD>(exStyle)));
winrt::check_bool(SetWindowPos(
m_window, HWND_TOPMOST, 0, 0, dest.right - dest.left, dest.bottom - dest.top, SWP_NOMOVE | SWP_SHOWWINDOW));
m_destRect = { 0, 0, cropWidth, cropHeight };
m_captured = true;
InvalidateRect(m_window, nullptr, FALSE);
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include <robmikh.common/DesktopWindow.h>
#include "CropAndLockWindow.h"
struct ScreenshotCropAndLockWindow : robmikh::common::desktop::DesktopWindow<ScreenshotCropAndLockWindow>, CropAndLockWindow
{
static const std::wstring ClassName;
ScreenshotCropAndLockWindow(std::wstring const& titleString, int width, int height);
~ScreenshotCropAndLockWindow() override;
LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam);
HWND Handle() override { return m_window; }
void CropAndLock(HWND windowToCrop, RECT cropRect) override;
void OnClosed(std::function<void(HWND)> callback) override { m_closedCallback = callback; }
private:
static void RegisterWindowClass();
private:
std::unique_ptr<void, decltype(&DeleteObject)> m_bitmap{ nullptr, &DeleteObject };
RECT m_destRect = {};
RECT m_sourceRect = {};
bool m_captured = false;
bool m_destroyed = false;
std::function<void(HWND)> m_closedCallback;
};

View File

@@ -4,4 +4,5 @@ enum class CropAndLockType
{ {
Reparent, Reparent,
Thumbnail, Thumbnail,
Screenshot,
}; };

View File

@@ -2,6 +2,7 @@
#include "SettingsWindow.h" #include "SettingsWindow.h"
#include "OverlayWindow.h" #include "OverlayWindow.h"
#include "CropAndLockWindow.h" #include "CropAndLockWindow.h"
#include "ScreenshotCropAndLockWindow.h"
#include "ThumbnailCropAndLockWindow.h" #include "ThumbnailCropAndLockWindow.h"
#include "ReparentCropAndLockWindow.h" #include "ReparentCropAndLockWindow.h"
#include "ModuleConstants.h" #include "ModuleConstants.h"
@@ -133,6 +134,7 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
// Handles and thread for the events sent from runner // Handles and thread for the events sent from runner
HANDLE m_reparent_event_handle; HANDLE m_reparent_event_handle;
HANDLE m_thumbnail_event_handle; HANDLE m_thumbnail_event_handle;
HANDLE m_screenshot_event_handle;
HANDLE m_exit_event_handle; HANDLE m_exit_event_handle;
std::thread m_event_triggers_thread; std::thread m_event_triggers_thread;
@@ -181,6 +183,11 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
Logger::trace(L"Creating a thumbnail window"); Logger::trace(L"Creating a thumbnail window");
Trace::CropAndLock::CreateThumbnailWindow(); Trace::CropAndLock::CreateThumbnailWindow();
break; break;
case CropAndLockType::Screenshot:
croppedWindow = std::make_shared<ScreenshotCropAndLockWindow>(title, 800, 600);
Logger::trace(L"Creating a screenshot window");
Trace::CropAndLock::CreateScreenshotWindow();
break;
default: default:
return; return;
} }
@@ -215,8 +222,9 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
// Start a thread to listen on the events. // Start a thread to listen on the events.
m_reparent_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT); m_reparent_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT);
m_thumbnail_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_THUMBNAIL_EVENT); m_thumbnail_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_THUMBNAIL_EVENT);
m_screenshot_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_SCREENSHOT_EVENT);
m_exit_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_EXIT_EVENT); m_exit_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_EXIT_EVENT);
if (!m_reparent_event_handle || !m_thumbnail_event_handle || !m_exit_event_handle) if (!m_reparent_event_handle || !m_thumbnail_event_handle || !m_screenshot_event_handle || !m_exit_event_handle)
{ {
Logger::warn(L"Failed to create events. {}", get_last_error_or_default(GetLastError())); Logger::warn(L"Failed to create events. {}", get_last_error_or_default(GetLastError()));
return 1; return 1;
@@ -224,10 +232,10 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
m_event_triggers_thread = std::thread([&]() { m_event_triggers_thread = std::thread([&]() {
MSG msg; MSG msg;
HANDLE event_handles[3] = { m_reparent_event_handle, m_thumbnail_event_handle, m_exit_event_handle }; HANDLE event_handles[4] = { m_reparent_event_handle, m_thumbnail_event_handle, m_screenshot_event_handle, m_exit_event_handle };
while (m_running) while (m_running)
{ {
DWORD dwEvt = MsgWaitForMultipleObjects(3, event_handles, false, INFINITE, QS_ALLINPUT); DWORD dwEvt = MsgWaitForMultipleObjects(4, event_handles, false, INFINITE, QS_ALLINPUT);
if (!m_running) if (!m_running)
{ {
break; break;
@@ -259,13 +267,25 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
break; break;
} }
case WAIT_OBJECT_0 + 2: case WAIT_OBJECT_0 + 2:
{
// Screenshot Event
bool enqueueSucceeded = controller.DispatcherQueue().TryEnqueue([&]() {
ProcessCommand(CropAndLockType::Screenshot);
});
if (!enqueueSucceeded)
{
Logger::error("Couldn't enqueue message to screenshot a window.");
}
break;
}
case WAIT_OBJECT_0 + 3:
{ {
// Exit Event // Exit Event
Logger::trace(L"Received an exit event."); Logger::trace(L"Received an exit event.");
PostThreadMessage(mainThreadId, WM_QUIT, 0, 0); PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
break; break;
} }
case WAIT_OBJECT_0 + 3: case WAIT_OBJECT_0 + 4:
if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{ {
TranslateMessage(&msg); TranslateMessage(&msg);
@@ -295,6 +315,7 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
SetEvent(m_reparent_event_handle); SetEvent(m_reparent_event_handle);
CloseHandle(m_reparent_event_handle); CloseHandle(m_reparent_event_handle);
CloseHandle(m_thumbnail_event_handle); CloseHandle(m_thumbnail_event_handle);
CloseHandle(m_screenshot_event_handle);
CloseHandle(m_exit_event_handle); CloseHandle(m_exit_event_handle);
m_event_triggers_thread.join(); m_event_triggers_thread.join();

View File

@@ -41,6 +41,15 @@ void Trace::CropAndLock::ActivateThumbnail() noexcept
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
} }
void Trace::CropAndLock::ActivateScreenshot() noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"CropAndLock_ActivateScreenshot",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
void Trace::CropAndLock::CreateReparentWindow() noexcept void Trace::CropAndLock::CreateReparentWindow() noexcept
{ {
TraceLoggingWriteWrapper( TraceLoggingWriteWrapper(
@@ -59,8 +68,17 @@ void Trace::CropAndLock::CreateThumbnailWindow() noexcept
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
} }
void Trace::CropAndLock::CreateScreenshotWindow() noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"CropAndLock_CreateScreenshotWindow",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
// Event to send settings telemetry. // Event to send settings telemetry.
void Trace::CropAndLock::SettingsTelemetry(PowertoyModuleIface::Hotkey& reparentHotkey, PowertoyModuleIface::Hotkey& thumbnailHotkey) noexcept void Trace::CropAndLock::SettingsTelemetry(PowertoyModuleIface::Hotkey& reparentHotkey, PowertoyModuleIface::Hotkey& thumbnailHotkey, PowertoyModuleIface::Hotkey& screenshotHotkey) noexcept
{ {
std::wstring hotKeyStrReparent = std::wstring hotKeyStrReparent =
std::wstring(reparentHotkey.win ? L"Win + " : L"") + std::wstring(reparentHotkey.win ? L"Win + " : L"") +
@@ -76,11 +94,19 @@ void Trace::CropAndLock::SettingsTelemetry(PowertoyModuleIface::Hotkey& reparent
std::wstring(thumbnailHotkey.alt ? L"Alt + " : L"") + std::wstring(thumbnailHotkey.alt ? L"Alt + " : L"") +
std::wstring(L"VK ") + std::to_wstring(thumbnailHotkey.key); std::wstring(L"VK ") + std::to_wstring(thumbnailHotkey.key);
std::wstring hotKeyStrScreenshot =
std::wstring(screenshotHotkey.win ? L"Win + " : L"") +
std::wstring(screenshotHotkey.ctrl ? L"Ctrl + " : L"") +
std::wstring(screenshotHotkey.shift ? L"Shift + " : L"") +
std::wstring(screenshotHotkey.alt ? L"Alt + " : L"") +
std::wstring(L"VK ") + std::to_wstring(screenshotHotkey.key);
TraceLoggingWriteWrapper( TraceLoggingWriteWrapper(
g_hProvider, g_hProvider,
"CropAndLock_Settings", "CropAndLock_Settings",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingWideString(hotKeyStrReparent.c_str(), "ReparentHotKey"), TraceLoggingWideString(hotKeyStrReparent.c_str(), "ReparentHotKey"),
TraceLoggingWideString(hotKeyStrThumbnail.c_str(), "ThumbnailHotkey")); TraceLoggingWideString(hotKeyStrThumbnail.c_str(), "ThumbnailHotkey"),
TraceLoggingWideString(hotKeyStrScreenshot.c_str(), "ScreenshotHotkey"));
} }

View File

@@ -12,8 +12,10 @@ public:
static void Enable(bool enabled) noexcept; static void Enable(bool enabled) noexcept;
static void ActivateReparent() noexcept; static void ActivateReparent() noexcept;
static void ActivateThumbnail() noexcept; static void ActivateThumbnail() noexcept;
static void ActivateScreenshot() noexcept;
static void CreateReparentWindow() noexcept; static void CreateReparentWindow() noexcept;
static void CreateThumbnailWindow() noexcept; static void CreateThumbnailWindow() noexcept;
static void SettingsTelemetry(PowertoyModuleIface::Hotkey&, PowertoyModuleIface::Hotkey&) noexcept; static void CreateScreenshotWindow() noexcept;
static void SettingsTelemetry(PowertoyModuleIface::Hotkey&, PowertoyModuleIface::Hotkey&, PowertoyModuleIface::Hotkey&) noexcept;
}; };
}; };

View File

@@ -29,6 +29,7 @@ namespace
const wchar_t JSON_KEY_CODE[] = L"code"; const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_REPARENT_HOTKEY[] = L"reparent-hotkey"; const wchar_t JSON_KEY_REPARENT_HOTKEY[] = L"reparent-hotkey";
const wchar_t JSON_KEY_THUMBNAIL_HOTKEY[] = L"thumbnail-hotkey"; const wchar_t JSON_KEY_THUMBNAIL_HOTKEY[] = L"thumbnail-hotkey";
const wchar_t JSON_KEY_SCREENSHOT_HOTKEY[] = L"screenshot-hotkey";
const wchar_t JSON_KEY_VALUE[] = L"value"; const wchar_t JSON_KEY_VALUE[] = L"value";
} }
@@ -124,6 +125,10 @@ public:
SetEvent(m_thumbnail_event_handle); SetEvent(m_thumbnail_event_handle);
Trace::CropAndLock::ActivateThumbnail(); Trace::CropAndLock::ActivateThumbnail();
} }
if (hotkeyId == 2) { // Same order as set by get_hotkeys
SetEvent(m_screenshot_event_handle);
Trace::CropAndLock::ActivateScreenshot();
}
return true; return true;
} }
@@ -133,12 +138,13 @@ public:
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{ {
if (hotkeys && buffer_size >= 2) if (hotkeys && buffer_size >= 3)
{ {
hotkeys[0] = m_reparent_hotkey; hotkeys[0] = m_reparent_hotkey;
hotkeys[1] = m_thumbnail_hotkey; hotkeys[1] = m_thumbnail_hotkey;
hotkeys[2] = m_screenshot_hotkey;
} }
return 2; return 3;
} }
// Enable the powertoy // Enable the powertoy
@@ -171,7 +177,7 @@ public:
virtual void send_settings_telemetry() override virtual void send_settings_telemetry() override
{ {
Logger::info("Send settings telemetry"); Logger::info("Send settings telemetry");
Trace::CropAndLock::SettingsTelemetry(m_reparent_hotkey, m_thumbnail_hotkey); Trace::CropAndLock::SettingsTelemetry(m_reparent_hotkey, m_thumbnail_hotkey, m_screenshot_hotkey);
} }
CropAndLockModuleInterface() CropAndLockModuleInterface()
@@ -182,6 +188,7 @@ public:
m_reparent_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT); m_reparent_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT);
m_thumbnail_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_THUMBNAIL_EVENT); m_thumbnail_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_THUMBNAIL_EVENT);
m_screenshot_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_SCREENSHOT_EVENT);
m_exit_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_EXIT_EVENT); m_exit_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_EXIT_EVENT);
init_settings(); init_settings();
@@ -202,6 +209,7 @@ private:
ResetEvent(m_reparent_event_handle); ResetEvent(m_reparent_event_handle);
ResetEvent(m_thumbnail_event_handle); ResetEvent(m_thumbnail_event_handle);
ResetEvent(m_screenshot_event_handle);
ResetEvent(m_exit_event_handle); ResetEvent(m_exit_event_handle);
SHELLEXECUTEINFOW sei{ sizeof(sei) }; SHELLEXECUTEINFOW sei{ sizeof(sei) };
@@ -234,6 +242,7 @@ private:
ResetEvent(m_reparent_event_handle); ResetEvent(m_reparent_event_handle);
ResetEvent(m_thumbnail_event_handle); ResetEvent(m_thumbnail_event_handle);
ResetEvent(m_screenshot_event_handle);
// Log telemetry // Log telemetry
if (traceEvent) if (traceEvent)
@@ -283,6 +292,21 @@ private:
{ {
Logger::error("Failed to initialize CropAndLock thumbnail shortcut from settings. Value will keep unchanged."); Logger::error("Failed to initialize CropAndLock thumbnail shortcut from settings. Value will keep unchanged.");
} }
try
{
Hotkey _temp_screenshot;
auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SCREENSHOT_HOTKEY).GetNamedObject(JSON_KEY_VALUE);
_temp_screenshot.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
_temp_screenshot.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
_temp_screenshot.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
_temp_screenshot.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
_temp_screenshot.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
m_screenshot_hotkey = _temp_screenshot;
}
catch (...)
{
Logger::error("Failed to initialize CropAndLock screenshot shortcut from settings. Value will keep unchanged.");
}
} }
else else
{ {
@@ -321,9 +345,11 @@ private:
// TODO: actual default hotkey setting in line with other PowerToys. // TODO: actual default hotkey setting in line with other PowerToys.
Hotkey m_reparent_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'R' }; Hotkey m_reparent_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'R' };
Hotkey m_thumbnail_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'T' }; Hotkey m_thumbnail_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'T' };
Hotkey m_screenshot_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'S' };
HANDLE m_reparent_event_handle; HANDLE m_reparent_event_handle;
HANDLE m_thumbnail_event_handle; HANDLE m_thumbnail_event_handle;
HANDLE m_screenshot_event_handle;
HANDLE m_exit_event_handle; HANDLE m_exit_event_handle;
}; };

View File

@@ -0,0 +1,248 @@
#include "pch.h"
#include "CLILogic.h"
#include <common/utils/json.h>
#include <iostream>
#include <sstream>
#include <chrono>
#include "resource.h"
#include <common/logger/logger.h>
#include <common/utils/logger_helper.h>
#include <type_traits>
template<typename T>
DWORD_PTR ToDwordPtr(T val)
{
if constexpr (std::is_pointer_v<T>)
{
return reinterpret_cast<DWORD_PTR>(val);
}
else
{
return static_cast<DWORD_PTR>(val);
}
}
template<typename... Args>
std::wstring FormatString(IStringProvider& strings, UINT id, Args... args)
{
std::wstring format = strings.GetString(id);
if (format.empty()) return L"";
DWORD_PTR arguments[] = { ToDwordPtr(args)..., 0 };
LPWSTR buffer = nullptr;
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ARGUMENT_ARRAY,
format.c_str(),
0,
0,
reinterpret_cast<LPWSTR>(&buffer),
0,
reinterpret_cast<va_list*>(arguments));
if (buffer)
{
std::wstring result(buffer);
LocalFree(buffer);
return result;
}
return L"";
}
std::wstring get_usage(IStringProvider& strings)
{
return strings.GetString(IDS_USAGE);
}
std::wstring get_json(const std::vector<ProcessResult>& results)
{
json::JsonObject root;
json::JsonArray processes;
for (const auto& result : results)
{
json::JsonObject process;
process.SetNamedValue(L"pid", json::JsonValue::CreateNumberValue(result.pid));
process.SetNamedValue(L"name", json::JsonValue::CreateStringValue(result.name));
process.SetNamedValue(L"user", json::JsonValue::CreateStringValue(result.user));
json::JsonArray files;
for (const auto& file : result.files)
{
files.Append(json::JsonValue::CreateStringValue(file));
}
process.SetNamedValue(L"files", files);
processes.Append(process);
}
root.SetNamedValue(L"processes", processes);
return root.Stringify().c_str();
}
std::wstring get_text(const std::vector<ProcessResult>& results, IStringProvider& strings)
{
std::wstringstream ss;
if (results.empty())
{
ss << strings.GetString(IDS_NO_PROCESSES);
return ss.str();
}
ss << strings.GetString(IDS_HEADER);
for (const auto& result : results)
{
ss << result.pid << L"\t"
<< result.user << L"\t"
<< result.name << std::endl;
}
return ss.str();
}
std::wstring kill_processes(const std::vector<ProcessResult>& results, IProcessTerminator& terminator, IStringProvider& strings)
{
std::wstringstream ss;
for (const auto& result : results)
{
if (terminator.terminate(result.pid))
{
ss << FormatString(strings, IDS_TERMINATED, result.pid, result.name.c_str());
}
else
{
ss << FormatString(strings, IDS_FAILED_TERMINATE, result.pid, result.name.c_str());
}
}
return ss.str();
}
CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IProcessTerminator& terminator, IStringProvider& strings)
{
Logger::info("Parsing arguments");
if (argc < 2)
{
Logger::warn("No arguments provided");
return { 1, get_usage(strings) };
}
bool json_output = false;
bool kill = false;
bool wait = false;
int timeout_ms = -1;
std::vector<std::wstring> paths;
for (int i = 1; i < argc; ++i)
{
std::wstring arg = argv[i];
if (arg == L"--json")
{
json_output = true;
}
else if (arg == L"--kill")
{
kill = true;
}
else if (arg == L"--wait")
{
wait = true;
}
else if (arg == L"--timeout")
{
if (i + 1 < argc)
{
try
{
timeout_ms = std::stoi(argv[++i]);
}
catch (...)
{
Logger::error("Invalid timeout value");
return { 1, strings.GetString(IDS_ERROR_INVALID_TIMEOUT) };
}
}
else
{
Logger::error("Timeout argument missing");
return { 1, strings.GetString(IDS_ERROR_TIMEOUT_ARG) };
}
}
else if (arg == L"--help")
{
return { 0, get_usage(strings) };
}
else
{
paths.push_back(arg);
}
}
if (paths.empty())
{
Logger::error("No paths specified");
return { 1, strings.GetString(IDS_ERROR_NO_PATHS) };
}
Logger::info("Processing {} paths", paths.size());
if (wait)
{
std::wstringstream ss;
if (json_output)
{
Logger::warn("Wait is incompatible with JSON output");
ss << strings.GetString(IDS_WARN_JSON_WAIT);
json_output = false;
}
ss << strings.GetString(IDS_WAITING);
auto start_time = std::chrono::steady_clock::now();
while (true)
{
auto results = finder.find(paths);
if (results.empty())
{
Logger::info("Files unlocked");
ss << strings.GetString(IDS_UNLOCKED);
break;
}
if (timeout_ms >= 0)
{
auto current_time = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - start_time).count();
if (elapsed > timeout_ms)
{
Logger::warn("Timeout waiting for files to be unlocked");
ss << strings.GetString(IDS_TIMEOUT);
return { 1, ss.str() };
}
}
Sleep(200);
}
return { 0, ss.str() };
}
auto results = finder.find(paths);
Logger::info("Found {} processes locking the files", results.size());
std::wstringstream output_ss;
if (kill)
{
Logger::info("Killing processes");
output_ss << kill_processes(results, terminator, strings);
// Re-check after killing
results = finder.find(paths);
Logger::info("Remaining processes: {}", results.size());
}
if (json_output)
{
output_ss << get_json(results) << std::endl;
}
else
{
output_ss << get_text(results, strings);
}
return { 0, output_ss.str() };
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <vector>
#include <string>
#include "FileLocksmithLib/FileLocksmith.h"
#include <Windows.h>
struct CommandResult
{
int exit_code;
std::wstring output;
};
struct IProcessFinder
{
virtual std::vector<ProcessResult> find(const std::vector<std::wstring>& paths) = 0;
virtual ~IProcessFinder() = default;
};
struct IProcessTerminator
{
virtual bool terminate(DWORD pid) = 0;
virtual ~IProcessTerminator() = default;
};
struct IStringProvider
{
virtual std::wstring GetString(UINT id) = 0;
virtual ~IStringProvider() = default;
};
CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IProcessTerminator& terminator, IStringProvider& strings);

View File

@@ -0,0 +1,19 @@
#include "resource.h"
#include <windows.h>
STRINGTABLE
BEGIN
IDS_USAGE "Usage: FileLocksmithCLI.exe [options] <path1> [path2] ...\nOptions:\n --kill Kill processes locking the files\n --json Output results in JSON format\n --wait Wait for files to be unlocked\n --timeout Timeout in milliseconds for --wait\n --help Show this help message\n"
IDS_NO_PROCESSES "No processes found locking the file(s).\n"
IDS_HEADER "PID\tUser\tProcess\n"
IDS_TERMINATED "Terminated process %1!d! (%2)\n"
IDS_FAILED_TERMINATE "Failed to terminate process %1!d! (%2)\n"
IDS_FAILED_OPEN "Failed to open process %1!d! (%2)\n"
IDS_ERROR_NO_PATHS "Error: No paths specified.\n"
IDS_WARN_JSON_WAIT "Warning: --wait is incompatible with --json. Ignoring --json.\n"
IDS_WAITING "Waiting for files to be unlocked...\n"
IDS_UNLOCKED "Files unlocked.\n"
IDS_TIMEOUT "Timeout waiting for files to be unlocked.\n"
IDS_ERROR_INVALID_TIMEOUT "Error: Invalid timeout value.\n"
IDS_ERROR_TIMEOUT_ARG "Error: --timeout requires an argument.\n"
END

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{49D456D3-F485-45AF-8875-45B44F193DDC}</ProjectGuid>
<RootNamespace>FileLocksmithCLI</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>FileLocksmithCLI</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<AdditionalIncludeDirectories>$(ProjectDir)..;$(ProjectDir)..\..\..;$(ProjectDir)..\..;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RunCodeAnalysis>false</RunCodeAnalysis>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>shlwapi.lib;ntdll.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<AdditionalIncludeDirectories>$(ProjectDir)..;$(ProjectDir)..\..\..;$(ProjectDir)..\..;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RunCodeAnalysis>false</RunCodeAnalysis>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>shlwapi.lib;ntdll.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="CLILogic.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="CLILogic.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="FileLocksmithCLI.rc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FileLocksmithLib\FileLocksmithLib.vcxproj">
<Project>{9d52fd25-ef90-4f9a-a015-91efc5daf54f}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\version\version.vcxproj">
<Project>{1248566c-272a-43c0-88d6-e6675d569a09}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="CLILogic.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="CLILogic.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,71 @@
#include "pch.h"
#include "CLILogic.h"
#include "FileLocksmithLib/FileLocksmith.h"
#include <iostream>
#include "resource.h"
#include <common/logger/logger.h>
#include <common/utils/logger_helper.h>
struct RealProcessFinder : IProcessFinder
{
std::vector<ProcessResult> find(const std::vector<std::wstring>& paths) override
{
return find_processes_recursive(paths);
}
};
struct RealProcessTerminator : IProcessTerminator
{
bool terminate(DWORD pid) override
{
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (hProcess)
{
bool result = TerminateProcess(hProcess, 0);
CloseHandle(hProcess);
return result;
}
return false;
}
};
struct RealStringProvider : IStringProvider
{
std::wstring GetString(UINT id) override
{
wchar_t buffer[4096];
int len = LoadStringW(GetModuleHandle(NULL), id, buffer, ARRAYSIZE(buffer));
if (len > 0)
{
return std::wstring(buffer, len);
}
return L"";
}
};
#ifndef UNIT_TEST
int wmain(int argc, wchar_t* argv[])
{
winrt::init_apartment();
LoggerHelpers::init_logger(L"FileLocksmithCLI", L"", LogSettings::fileLocksmithLoggerName);
Logger::info("FileLocksmithCLI started");
RealProcessFinder finder;
RealProcessTerminator terminator;
RealStringProvider strings;
auto result = run_command(argc, argv, finder, terminator, strings);
if (result.exit_code != 0)
{
Logger::error("Command failed with exit code {}", result.exit_code);
}
else
{
Logger::info("Command succeeded");
}
std::wcout << result.output;
return result.exit_code;
}
#endif

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,22 @@
#pragma once
#ifndef PCH_H
#define PCH_H
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <winternl.h>
#include <Psapi.h>
#include <shellapi.h>
#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <algorithm>
#include <winrt/base.h>
#endif // PCH_H

View File

@@ -0,0 +1,16 @@
// resource.h
#pragma once
#define IDS_USAGE 101
#define IDS_NO_PROCESSES 102
#define IDS_HEADER 103
#define IDS_TERMINATED 104
#define IDS_FAILED_TERMINATE 105
#define IDS_FAILED_OPEN 106
#define IDS_ERROR_NO_PATHS 107
#define IDS_WARN_JSON_WAIT 108
#define IDS_WAITING 109
#define IDS_UNLOCKED 110
#define IDS_TIMEOUT 111
#define IDS_ERROR_INVALID_TIMEOUT 112
#define IDS_ERROR_TIMEOUT_ARG 113

View File

@@ -0,0 +1,130 @@
#include "pch.h"
#include "CppUnitTest.h"
#include "../CLILogic.h"
#include <map>
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace FileLocksmithCLIUnitTests
{
struct MockProcessFinder : IProcessFinder
{
std::vector<ProcessResult> results;
std::vector<ProcessResult> find(const std::vector<std::wstring>& paths) override
{
(void)paths;
return results;
}
};
struct MockProcessTerminator : IProcessTerminator
{
bool shouldSucceed = true;
std::vector<DWORD> terminatedPids;
bool terminate(DWORD pid) override
{
terminatedPids.push_back(pid);
return shouldSucceed;
}
};
struct MockStringProvider : IStringProvider
{
std::map<UINT, std::wstring> strings;
std::wstring GetString(UINT id) override
{
if (strings.count(id)) return strings[id];
return L"String_" + std::to_wstring(id);
}
};
TEST_CLASS(CLITests)
{
public:
TEST_METHOD(TestNoArgs)
{
MockProcessFinder finder;
MockProcessTerminator terminator;
MockStringProvider strings;
wchar_t* argv[] = { (wchar_t*)L"exe" };
auto result = run_command(1, argv, finder, terminator, strings);
Assert::AreEqual(1, result.exit_code);
}
TEST_METHOD(TestHelp)
{
MockProcessFinder finder;
MockProcessTerminator terminator;
MockStringProvider strings;
wchar_t* argv[] = { (wchar_t*)L"exe", (wchar_t*)L"--help" };
auto result = run_command(2, argv, finder, terminator, strings);
Assert::AreEqual(0, result.exit_code);
}
TEST_METHOD(TestFindProcesses)
{
MockProcessFinder finder;
finder.results = { { L"process", 123, L"user", { L"file1" } } };
MockProcessTerminator terminator;
MockStringProvider strings;
wchar_t* argv[] = { (wchar_t*)L"exe", (wchar_t*)L"file1" };
auto result = run_command(2, argv, finder, terminator, strings);
Assert::AreEqual(0, result.exit_code);
Assert::IsTrue(result.output.find(L"123") != std::wstring::npos);
Assert::IsTrue(result.output.find(L"process") != std::wstring::npos);
}
TEST_METHOD(TestJsonOutput)
{
MockProcessFinder finder;
finder.results = { { L"process", 123, L"user", { L"file1" } } };
MockProcessTerminator terminator;
MockStringProvider strings;
wchar_t* argv[] = { (wchar_t*)L"exe", (wchar_t*)L"file1", (wchar_t*)L"--json" };
auto result = run_command(3, argv, finder, terminator, strings);
Microsoft::VisualStudio::CppUnitTestFramework::Logger::WriteMessage(result.output.c_str());
Assert::AreEqual(0, result.exit_code);
Assert::IsTrue(result.output.find(L"\"pid\"") != std::wstring::npos);
Assert::IsTrue(result.output.find(L"123") != std::wstring::npos);
}
TEST_METHOD(TestKill)
{
MockProcessFinder finder;
finder.results = { { L"process", 123, L"user", { L"file1" } } };
MockProcessTerminator terminator;
MockStringProvider strings;
wchar_t* argv[] = { (wchar_t*)L"exe", (wchar_t*)L"file1", (wchar_t*)L"--kill" };
auto result = run_command(3, argv, finder, terminator, strings);
Assert::AreEqual(0, result.exit_code);
Assert::AreEqual((size_t)1, terminator.terminatedPids.size());
Assert::AreEqual((DWORD)123, terminator.terminatedPids[0]);
}
TEST_METHOD(TestTimeout)
{
MockProcessFinder finder;
// Always return results so it waits
finder.results = { { L"process", 123, L"user", { L"file1" } } };
MockProcessTerminator terminator;
MockStringProvider strings;
wchar_t* argv[] = { (wchar_t*)L"exe", (wchar_t*)L"file1", (wchar_t*)L"--wait", (wchar_t*)L"--timeout", (wchar_t*)L"100" };
auto result = run_command(5, argv, finder, terminator, strings);
Assert::AreEqual(1, result.exit_code);
}
};
}

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<ProjectGuid>{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>FileLocksmithCLIUnitTests</RootNamespace>
<ProjectName>FileLocksmithCLI.UnitTests</ProjectName>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="..\..\..\..\..\deps\spdlog.props" />
<PropertyGroup Label="Configuration">
<PlatformToolset>v143</PlatformToolset>
<ConfigurationType>DynamicLibrary</ConfigurationType>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\..\$(Platform)\$(Configuration)\tests\FileLocksmithCLI\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\;..\..\;..\..\..\..\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;UNIT_TEST;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<UseFullPaths>true</UseFullPaths>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<DisableSpecificWarnings>26466;%(DisableSpecificWarnings)</DisableSpecificWarnings>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>shlwapi.lib;ntdll.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="FileLocksmithCLITests.cpp" />
<ClCompile Include="..\CLILogic.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\FileLocksmithLib\FileLocksmithLib.vcxproj">
<Project>{9d52fd25-ef90-4f9a-a015-91efc5daf54f}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\..\common\version\version.vcxproj">
<Project>{1248566c-272a-43c0-88d6-e6675d569a09}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,9 @@
#pragma once
#include <winrt/base.h>
#include <Windows.h>
#include <shellapi.h>
#include <string>
#include <vector>
#include <iostream>
#include <sstream>
#include "CppUnitTest.h"

View File

@@ -0,0 +1,9 @@
#pragma once
#include "ProcessResult.h"
// Second version, checks handles towards files and all subfiles and folders of given dirs, if any.
std::vector<ProcessResult> find_processes_recursive(const std::vector<std::wstring>& paths);
// Gives the full path of the executable, given the process id
std::wstring pid_to_full_path(DWORD pid);

View File

@@ -34,9 +34,9 @@
<ClCompile> <ClCompile>
<WarningLevel>Level3</WarningLevel> <WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck> <SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>WIN32;_DEBUG;_LIB;FILELOCKSMITH_LIB_STATIC;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode> <ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>../../..;../..;</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\FileLocksmithLibInterop;../../..;../..;</AdditionalIncludeDirectories>
</ClCompile> </ClCompile>
<Link> <Link>
<SubSystem> <SubSystem>
@@ -50,9 +50,9 @@
<FunctionLevelLinking>true</FunctionLevelLinking> <FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions> <IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck> <SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>WIN32;NDEBUG;_LIB;FILELOCKSMITH_LIB_STATIC;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode> <ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>../../..;../..;</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\FileLocksmithLibInterop;../../..;../..;</AdditionalIncludeDirectories>
</ClCompile> </ClCompile>
<Link> <Link>
<SubSystem> <SubSystem>
@@ -68,13 +68,15 @@
<ClInclude Include="Settings.h" /> <ClInclude Include="Settings.h" />
<ClInclude Include="Trace.h" /> <ClInclude Include="Trace.h" />
<ClInclude Include="framework.h" /> <ClInclude Include="framework.h" />
<ClInclude Include="pch.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="IPC.cpp" /> <ClCompile Include="IPC.cpp" />
<ClCompile Include="Settings.cpp" /> <ClCompile Include="Settings.cpp" />
<ClCompile Include="Trace.cpp" /> <ClCompile Include="Trace.cpp" />
<ClCompile Include="FileLocksmithLib.cpp" /> <ClCompile Include="FileLocksmithLib.cpp" />
<ClCompile Include="..\FileLocksmithLibInterop\FileLocksmith.cpp" />
<ClCompile Include="..\FileLocksmithLibInterop\NtdllBase.cpp" />
<ClCompile Include="..\FileLocksmithLibInterop\NtdllExtensions.cpp" />
<ClCompile Include="pch.cpp"> <ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile> </ClCompile>

View File

@@ -38,6 +38,15 @@
<ClCompile Include="FileLocksmithLib.cpp"> <ClCompile Include="FileLocksmithLib.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="FileLocksmith.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="NtdllBase.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="NtdllExtensions.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp"> <ClCompile Include="pch.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>

View File

@@ -0,0 +1,12 @@
#pragma once
#include <string>
#include <vector>
#include <Windows.h>
struct ProcessResult
{
std::wstring name;
DWORD pid;
std::wstring user;
std::vector<std::wstring> files;
};

View File

@@ -2,6 +2,7 @@
#include "Settings.h" #include "Settings.h"
#include "Constants.h" #include "Constants.h"
#include <filesystem>
#include <common/utils/json.h> #include <common/utils/json.h>
#include <common/SettingsAPI/settings_helpers.h> #include <common/SettingsAPI/settings_helpers.h>

View File

@@ -1,13 +0,0 @@
// pch.h: This is a precompiled header file.
// Files listed below are compiled only once, improving build performance for future builds.
// This also affects IntelliSense performance, including code completion and many code browsing features.
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
// Do not add files here that you will be updating frequently as this negates the performance advantage.
#ifndef PCH_H
#define PCH_H
// add headers that you want to pre-compile here
#include "framework.h"
#endif //PCH_H

View File

@@ -18,4 +18,6 @@
#include <algorithm> #include <algorithm>
#include <fstream> #include <fstream>
#ifndef FILELOCKSMITH_LIB_STATIC
#include <winrt/PowerToys.Interop.h> #include <winrt/PowerToys.Interop.h>
#endif

View File

@@ -0,0 +1,23 @@
#pragma once
enum class SettingId
{
ScheduleMode = 0,
Latitude,
Longitude,
LightTime,
DarkTime,
Sunrise_Offset,
Sunset_Offset,
ChangeSystem,
ChangeApps,
WallpaperEnabled,
WallpaperVirtualDesktopEnabled,
WallpaperStyleLight,
WallpaperStyleDark,
WallpaperPathLight,
WallpaperPathDark
};
inline constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
inline constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate";

View File

@@ -0,0 +1,370 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <logger/logger_settings.h>
#include <logger/logger.h>
#include <utils/logger_helper.h>
#include "ThemeHelper.h"
#include <wil/resource.h>
#include "SettingsConstants.h"
static auto RegKeyGuard(HKEY& hKey) noexcept
{
return wil::scope_exit([&hKey]() {
if (hKey == nullptr)
return;
if (RegCloseKey(hKey) != ERROR_SUCCESS)
std::terminate();
});
}
// Controls changing the themes.
static void ResetColorPrevalence() noexcept
{
HKEY hKey{};
auto closeKey = RegKeyGuard(hKey);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = 0; // back to default value
RegSetValueEx(hKey, L"ColorPrevalence", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_DWMCOLORIZATIONCOLORCHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetAppsTheme(bool mode) noexcept
{
HKEY hKey{};
auto closeKey = RegKeyGuard(hKey);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"AppsUseLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetSystemTheme(bool mode) noexcept
{
HKEY hKey{};
auto closeKey = RegKeyGuard(hKey);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
if (mode) // if are changing to light mode
{
ResetColorPrevalence();
Logger::info(L"[LightSwitchService] Reset ColorPrevalence to default when switching to light mode.");
}
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
// Can think of this as "is the current theme light?"
bool GetCurrentSystemTheme() noexcept
{
HKEY hKey{};
auto closeKey = RegKeyGuard(hKey);
DWORD value = 1; // default = light
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"SystemUsesLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
}
return value == 1; // true = light, false = dark
}
bool GetCurrentAppsTheme() noexcept
{
HKEY hKey{};
auto closeKey = RegKeyGuard(hKey);
DWORD value = 1;
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
}
return value == 1; // true = light, false = dark
}
bool IsNightLightEnabled() noexcept
{
HKEY hKey{};
auto closeKey = RegKeyGuard(hKey);
if (RegOpenKeyExW(HKEY_CURRENT_USER, NIGHT_LIGHT_REGISTRY_PATH, 0, KEY_READ, &hKey) != ERROR_SUCCESS)
return false;
// RegGetValueW will set size to the size of the data and we expect that to be at least 25 bytes (we need to access bytes 23 and 24)
DWORD size = 0;
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, nullptr, &size) != ERROR_SUCCESS || size < 25)
{
return false;
}
std::vector<BYTE> data(size);
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, data.data(), &size) != ERROR_SUCCESS)
{
return false;
}
return data[23] == 0x10 && data[24] == 0x00;
}
#include <atomic>
#include <charconv>
#include <array>
#include <string>
static bool GetWindowsVersionFromRegistryInternal(int& build, int& revision) noexcept
{
HKEY hKey{};
auto closeKey = RegKeyGuard(hKey);
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hKey) != ERROR_SUCCESS)
{
return false;
}
wchar_t buffer[11]{};
DWORD bufferSize{ sizeof(buffer) };
if (RegGetValueW(hKey, nullptr, L"CurrentBuildNumber", RRF_RT_REG_SZ, nullptr, static_cast<void*>(buffer), &bufferSize))
{
return false;
}
char bufferA[11]{};
std::transform(std::begin(buffer), std::end(buffer), std::begin(bufferA), [](auto c) { return static_cast<char>(c); });
int bld{};
if (std::from_chars(bufferA, bufferA + sizeof(bufferA), bld).ec != std::errc{})
{
return false;
}
DWORD rev{};
DWORD revSize{ sizeof(rev) };
if (RegGetValueW(hKey, nullptr, L"UBR", RRF_RT_DWORD, nullptr, &rev, &revSize) != ERROR_SUCCESS)
{
return false;
}
revision = static_cast<int>(rev);
build = static_cast<int>(bld);
return true;
}
static bool GetWindowsVersionFromRegistry(int& build, int& revision) noexcept
{
static std::atomic<int> build_cache{};
static std::atomic<int> rev_cache{};
if (auto bld = build_cache.load(); bld != 0)
{
build = bld;
revision = rev_cache.load();
return true;
}
int bld{};
int rev{};
if (auto e = GetWindowsVersionFromRegistryInternal(bld, rev); e == false)
{
return e;
}
build = bld;
revision = rev;
rev_cache.store(rev);
// Write after rev_cache for condition
build_cache.store(bld);
return true;
}
// This function will supplement the wallpaper path setting. It does not cause the wallpaper to change, but for consistency, it is better to set it
static int SetRemainWallpaperPathRegistry(std::wstring const& wallpaperPath) noexcept
{
HKEY hKey{};
auto closeKey = RegKeyGuard(hKey);
if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Wallpapers", 0, KEY_WRITE, &hKey) != ERROR_SUCCESS)
{
// The key may not exist after updating Windows, so it is not an error
// The key will be created by the Settings app
return 0;
}
if (RegSetValueExW(hKey, L"CurrentWallpaperPath", 0, REG_SZ, reinterpret_cast<const BYTE*>(wallpaperPath.data()), static_cast<DWORD>((wallpaperPath.size() + 1u) * sizeof(wchar_t))) != ERROR_SUCCESS)
{
return 0x301;
}
DWORD backgroundType = 0; // 0 = picture, 1 = solid color, 2 = slideshow
if (RegSetValueExW(hKey, L"BackgroundType", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&backgroundType), static_cast<DWORD>(sizeof(DWORD))) != ERROR_SUCCESS)
{
return 0x302;
}
return 0;
}
#define WIN32_LEAN_AND_MEAN
#define COM_NO_WINDOWS_H
#include <string>
#include <unknwn.h>
#include <inspectable.h>
#include <restrictederrorinfo.h>
#include "Shobjidl.h"
#include <hstring.h>
#include <winrt/base.h>
#pragma comment(lib, "runtimeobject.lib")
// COM interface definition from https://github.com/MScholtes/VirtualDesktop
inline constexpr GUID CLSID_ImmersiveShell{ 0xC2F03A33, 0x21F5, 0x47FA, { 0xB4, 0xBB, 0x15, 0x63, 0x62, 0xA2, 0xF2, 0x39 } };
inline constexpr GUID CLSID_VirtualDesktopManagerInternal{ 0xC5E0CDCA, 0x7B6E, 0x41B2, { 0x9F, 0xC4, 0xD9, 0x39, 0x75, 0xCC, 0x46, 0x7B } };
struct __declspec(novtable) __declspec(uuid("6D5140C1-7436-11CE-8034-00AA006009FA")) IServiceProvider10 : public IUnknown
{
virtual HRESULT __stdcall QueryService(REFGUID service, REFIID riid, void** obj) = 0;
};
#undef CreateDesktop
struct __declspec(novtable) __declspec(uuid("53F5CA0B-158F-4124-900C-057158060B27")) IVirtualDesktopManagerInternal24H2 : public IUnknown
{
virtual HRESULT __stdcall GetCount(int* count) = 0;
virtual HRESULT __stdcall MoveViewToDesktop(IInspectable* view, IUnknown* desktop) = 0;
virtual HRESULT __stdcall CanViewMoveDesktops(IInspectable* view, bool* result) = 0;
virtual HRESULT __stdcall GetCurrentDesktop(IUnknown** desktop) = 0;
virtual HRESULT __stdcall GetDesktops(IObjectArray** desktops) = 0;
virtual HRESULT __stdcall GetAdjacentDesktop(IUnknown* from, int direction, IUnknown** desktop) = 0;
virtual HRESULT __stdcall SwitchDesktop(IUnknown* desktop) = 0;
virtual HRESULT __stdcall SwitchDesktopAndMoveForegroundView(IUnknown* desktop) = 0;
virtual HRESULT __stdcall CreateDesktop(IUnknown** desktop) = 0;
virtual HRESULT __stdcall MoveDesktop(IUnknown* desktop, int nIndex) = 0;
virtual HRESULT __stdcall RemoveDesktop(IUnknown* desktop, IUnknown* fallback) = 0;
virtual HRESULT __stdcall FindDesktop(const GUID* desktopId, IUnknown** desktop) = 0;
virtual HRESULT __stdcall GetDesktopSwitchIncludeExcludeViews(IUnknown* desktop, IObjectArray** unknown1, IObjectArray** unknown2) = 0;
virtual HRESULT __stdcall SetDesktopName(IUnknown* desktop, HSTRING name) = 0;
virtual HRESULT __stdcall SetDesktopWallpaper(IUnknown* desktop, HSTRING path) = 0;
virtual HRESULT __stdcall UpdateWallpaperPathForAllDesktops(HSTRING path) = 0;
virtual HRESULT __stdcall CopyDesktopState(IInspectable* pView0, IInspectable* pView1) = 0;
virtual HRESULT __stdcall CreateRemoteDesktop(HSTRING path, IUnknown** desktop) = 0;
virtual HRESULT __stdcall SwitchRemoteDesktop(IUnknown* desktop, void* switchType) = 0;
virtual HRESULT __stdcall SwitchDesktopWithAnimation(IUnknown* desktop) = 0;
virtual HRESULT __stdcall GetLastActiveDesktop(IUnknown** desktop) = 0;
virtual HRESULT __stdcall WaitForAnimationToComplete() = 0;
};
// Using this method to set the wallpaper works across virtual desktops, but it does not provide the functionality to set the style
static int SetWallpaperViaIVirtualDesktopManagerInternal(const std::wstring& path) noexcept
{
int build{};
int revision{};
if (!GetWindowsVersionFromRegistry(build, revision))
{
return 0x201;
}
// Unstable Windows internal API, at least 24H2 required
if (build < 26100)
{
return 0x202;
}
auto shell = winrt::try_create_instance<IServiceProvider10>(CLSID_ImmersiveShell, CLSCTX_LOCAL_SERVER);
if (!shell)
{
return 0x203;
}
winrt::com_ptr<IVirtualDesktopManagerInternal24H2> virtualDesktopManagerInternal;
if (shell->QueryService(
CLSID_VirtualDesktopManagerInternal,
__uuidof(IVirtualDesktopManagerInternal24H2),
virtualDesktopManagerInternal.put_void()) != S_OK)
{
return 0x204;
}
if (virtualDesktopManagerInternal->UpdateWallpaperPathForAllDesktops(static_cast<HSTRING>(winrt::detach_abi(path))) != S_OK)
{
return 0x205;
}
return 0;
}
// After setting the wallpaper using this method, switching to other virtual desktops will cause the wallpaper to be restored
static int SetWallpaperViaIDesktopWallpaper(const std::wstring& path, int style) noexcept
{
auto pos = static_cast<DESKTOP_WALLPAPER_POSITION>(style);
switch (pos)
{
case DWPOS_CENTER:
case DWPOS_TILE:
case DWPOS_STRETCH:
case DWPOS_FIT:
case DWPOS_FILL:
case DWPOS_SPAN:
break;
default:
std::terminate();
}
auto desktopWallpaper = winrt::try_create_instance<IDesktopWallpaper>(__uuidof(DesktopWallpaper), CLSCTX_LOCAL_SERVER);
if (!desktopWallpaper)
{
return 0x301;
}
if (desktopWallpaper->SetPosition(pos) != S_OK)
{
return 0x302;
}
if (desktopWallpaper->SetWallpaper(nullptr, path.c_str()) != S_OK)
{
return 0x303;
}
return 0;
}
int SetDesktopWallpaper(const std::wstring& path, int style, bool virtualDesktop) noexcept
{
if (virtualDesktop)
{
if (auto e = SetWallpaperViaIVirtualDesktopManagerInternal(path); e != 0)
{
return e;
}
}
if (auto e = SetWallpaperViaIDesktopWallpaper(path, style); e != 0)
{
return e;
}
if (auto e = SetRemainWallpaperPathRegistry(path); e != 0)
{
return e;
}
return 0;
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include <string>
void SetSystemTheme(bool dark) noexcept;
void SetAppsTheme(bool dark) noexcept;
bool GetCurrentSystemTheme() noexcept;
bool GetCurrentAppsTheme() noexcept;
bool IsNightLightEnabled() noexcept;
// Returned 0 indicates success; otherwise, the reason is returned, see definition
int SetDesktopWallpaper(std::wstring const& wallpaperPath, int style, bool virtualDesktop) noexcept;

View File

@@ -166,13 +166,14 @@
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemDefinitionGroup> <ItemDefinitionGroup>
<ClCompile> <ClCompile>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\LightSwitchCommon;..\..\..\common;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile> </ClCompile>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
<ClInclude Include="resource.h" /> <ClInclude Include="resource.h" />
<ClInclude Include="ThemeHelper.h" /> <ClInclude Include="..\LightSwitchCommon\ThemeHelper.h" />
<ClInclude Include="..\LightSwitchCommon\SettingsConstants.h" />
<ClInclude Include="trace.h" /> <ClInclude Include="trace.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -187,7 +188,12 @@
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">pch.h</PrecompiledHeaderFile> <PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">pch.h</PrecompiledHeaderFile> <PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">pch.h</PrecompiledHeaderFile>
</ClCompile> </ClCompile>
<ClCompile Include="ThemeHelper.cpp" /> <ClCompile Include="..\LightSwitchCommon\ThemeHelper.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="trace.cpp" /> <ClCompile Include="trace.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -210,6 +216,7 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" /> <Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets"> <ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" /> <Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup> </ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">

View File

@@ -10,7 +10,7 @@
<ClCompile Include="trace.cpp"> <ClCompile Include="trace.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="ThemeHelper.cpp"> <ClCompile Include="..\LightSwitchCommon\ThemeHelper.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
</ItemGroup> </ItemGroup>
@@ -24,7 +24,10 @@
<ClInclude Include="trace.h"> <ClInclude Include="trace.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="ThemeHelper.h"> <ClInclude Include="..\LightSwitchCommon\ThemeHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\LightSwitchCommon\SettingsConstants.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
</ItemGroup> </ItemGroup>
@@ -47,4 +50,7 @@
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,106 +0,0 @@
#include "pch.h"
#include <windows.h>
#include "ThemeHelper.h"
// Controls changing the themes.
static void ResetColorPrevalence()
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = 0; // back to default value
RegSetValueEx(hKey, L"ColorPrevalence", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_DWMCOLORIZATIONCOLORCHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetAppsTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"AppsUseLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetSystemTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
if (mode) // if are changing to light mode
{
ResetColorPrevalence();
}
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
bool GetCurrentSystemTheme()
{
HKEY hKey;
DWORD value = 1; // default = light
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"SystemUsesLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
}
return value == 1; // true = light, false = dark
}
bool GetCurrentAppsTheme()
{
HKEY hKey;
DWORD value = 1;
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
}
return value == 1; // true = light, false = dark
}

View File

@@ -1,5 +0,0 @@
#pragma once
void SetSystemTheme(bool dark);
void SetAppsTheme(bool dark);
bool GetCurrentSystemTheme();
bool GetCurrentAppsTheme();

View File

@@ -94,6 +94,12 @@ struct ModuleSettings
int m_sunset_offset = 0; int m_sunset_offset = 0;
std::wstring m_latitude = L"0.0"; std::wstring m_latitude = L"0.0";
std::wstring m_longitude = L"0.0"; std::wstring m_longitude = L"0.0";
bool m_wallpaper = false;
bool m_wallpaper_virtual_desktop = false;
int m_wallpaper_style_light = 0;
int m_wallpaper_style_dark = 0;
std::wstring m_wallpaper_path_light;
std::wstring m_wallpaper_path_dark;
} g_settings; } g_settings;
class LightSwitchInterface : public PowertoyModuleIface class LightSwitchInterface : public PowertoyModuleIface
@@ -351,6 +357,30 @@ public:
{ {
g_settings.m_longitude = *v; g_settings.m_longitude = *v;
} }
if (auto v = values.get_bool_value(L"wallpaperEnabled"))
{
g_settings.m_wallpaper = *v;
}
if (auto v = values.get_bool_value(L"wallpaperVirtualDesktopEnabled"))
{
g_settings.m_wallpaper_virtual_desktop = *v;
}
if (auto v = values.get_int_value(L"wallpaperStyleLight"))
{
g_settings.m_wallpaper_style_light = *v;
}
if (auto v = values.get_int_value(L"wallpaperStyleDark"))
{
g_settings.m_wallpaper_style_dark = *v;
}
if (auto v = values.get_string_value(L"wallpaperPathLight"))
{
g_settings.m_wallpaper_path_light = *v;
}
if (auto v = values.get_string_value(L"wallpaperPathDark"))
{
g_settings.m_wallpaper_path_dark = *v;
}
values.save_to_settings_file(); values.save_to_settings_file();
} }
@@ -405,6 +435,7 @@ public:
{ {
m_enabled = true; m_enabled = true;
Logger::info(L"Enabling Light Switch module..."); Logger::info(L"Enabling Light Switch module...");
Trace::Enable(true);
unsigned long powertoys_pid = GetCurrentProcessId(); unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring args = L"--pid " + std::to_wstring(powertoys_pid); std::wstring args = L"--pid " + std::to_wstring(powertoys_pid);
@@ -482,7 +513,8 @@ public:
CloseHandle(m_process); CloseHandle(m_process);
m_process = nullptr; m_process = nullptr;
} }
Trace::Enable(false);
StopToggleListener(); StopToggleListener();
} }
@@ -539,6 +571,8 @@ public:
if (m_enabled) if (m_enabled)
{ {
Logger::trace(L"Light Switch hotkey pressed"); Logger::trace(L"Light Switch hotkey pressed");
Trace::ShortcutInvoked();
if (!is_process_running()) if (!is_process_running())
{ {
enable(); enable();
@@ -562,15 +596,53 @@ public:
}; };
static bool IsValidPath(const std::wstring& path)
{
std::error_code ec;
return !path.empty() && std::filesystem::exists(path, ec);
}
void LightSwitchInterface::ToggleTheme() void LightSwitchInterface::ToggleTheme()
{ {
bool current_system_theme = GetCurrentSystemTheme();
bool current_apps_theme = GetCurrentAppsTheme();
if (g_settings.m_changeSystem) if (g_settings.m_changeSystem)
{ {
SetSystemTheme(!GetCurrentSystemTheme()); SetSystemTheme(!current_system_theme);
} }
if (g_settings.m_changeApps) if (g_settings.m_changeApps)
{ {
SetAppsTheme(!GetCurrentAppsTheme()); SetAppsTheme(!current_apps_theme);
}
bool new_system_theme = GetCurrentSystemTheme();
bool new_apps_theme = GetCurrentAppsTheme();
bool changeWallpaper =
g_settings.m_wallpaper &&
IsValidPath(g_settings.m_wallpaper_path_light) &&
IsValidPath(g_settings.m_wallpaper_path_dark);
// if something changed and wallpaper change is enabled and paths are valid
if ((new_system_theme != current_system_theme || new_apps_theme != current_apps_theme) && changeWallpaper)
{
bool shouldBeLight;
if (g_settings.m_changeSystem && new_system_theme != current_system_theme)
{
shouldBeLight = new_system_theme;
}
else
{
// Either apps-only changed, or system didn't move
shouldBeLight = new_apps_theme;
}
auto&& wallpaperPath = shouldBeLight ? g_settings.m_wallpaper_path_light : g_settings.m_wallpaper_path_dark;
auto style = shouldBeLight ? g_settings.m_wallpaper_style_light : g_settings.m_wallpaper_style_dark;
SetDesktopWallpaper(wallpaperPath, style, g_settings.m_wallpaper_virtual_desktop);
} }
if (!m_manual_override_event_handle) if (!m_manual_override_event_handle)
@@ -689,6 +761,18 @@ void LightSwitchInterface::init_settings()
g_settings.m_latitude = *v; g_settings.m_latitude = *v;
if (auto v = settings.get_string_value(L"longitude")) if (auto v = settings.get_string_value(L"longitude"))
g_settings.m_longitude = *v; g_settings.m_longitude = *v;
if (auto v = settings.get_bool_value(L"wallpaperEnabled"))
g_settings.m_wallpaper = *v;
if (auto v = settings.get_bool_value(L"wallpaperVirtualDesktopEnabled"))
g_settings.m_wallpaper_virtual_desktop = *v;
if (auto v = settings.get_int_value(L"wallpaperStyleLight"))
g_settings.m_wallpaper_style_light = *v;
if (auto v = settings.get_int_value(L"wallpaperStyleDark"))
g_settings.m_wallpaper_style_dark = *v;
if (auto v = settings.get_string_value(L"wallpaperPathLight"))
g_settings.m_wallpaper_path_light = *v;
if (auto v = settings.get_string_value(L"wallpaperPathDark"))
g_settings.m_wallpaper_path_dark = *v;
Logger::info(L"[Light Switch] init_settings: loaded successfully"); Logger::info(L"[Light Switch] init_settings: loaded successfully");
} }

View File

@@ -19,12 +19,21 @@ void Trace::UnregisterProvider()
TraceLoggingUnregister(g_hProvider); TraceLoggingUnregister(g_hProvider);
} }
void Trace::MyEvent() void Trace::Enable(bool enabled) noexcept
{ {
TraceLoggingWrite( TraceLoggingWrite(
g_hProvider, g_hProvider,
"PowerToyName_MyEvent", "LightSwitch_EnableLightSwitch",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}
void Trace::ShortcutInvoked() noexcept
{
TraceLoggingWrite(
g_hProvider,
"LightSwitch_ShortcutInvoked",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
} }

View File

@@ -11,5 +11,6 @@ class Trace
public: public:
static void RegisterProvider(); static void RegisterProvider();
static void UnregisterProvider(); static void UnregisterProvider();
static void MyEvent(); static void Enable(bool enabled) noexcept;
static void ShortcutInvoked() noexcept;
}; };

View File

@@ -14,6 +14,7 @@
#include "LightSwitchStateManager.h" #include "LightSwitchStateManager.h"
#include <LightSwitchUtils.h> #include <LightSwitchUtils.h>
#include <NightLightRegistryObserver.h> #include <NightLightRegistryObserver.h>
#include <trace.h>
SERVICE_STATUS g_ServiceStatus = {}; SERVICE_STATUS g_ServiceStatus = {};
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr; SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
@@ -23,7 +24,7 @@ static LightSwitchStateManager* g_stateManagerPtr = nullptr;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv); VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl); VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam); DWORD WINAPI ServiceWorkerThread(LPVOID lpParam);
void ApplyTheme(bool shouldBeLight); void ApplyTheme(bool shouldBeLight, bool changeWallpaper);
// Entry point for the executable // Entry point for the executable
int _tmain(int argc, TCHAR* argv[]) int _tmain(int argc, TCHAR* argv[])
@@ -124,9 +125,29 @@ VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl)
} }
} }
void ApplyTheme(bool shouldBeLight) void SetWallpaper(bool shouldBeLight)
{
const auto& settings = LightSwitchSettings::settings();
if (settings.wallpaperEnabled)
{
std::wstring const& wallpaperPath = shouldBeLight ? settings.wallpaperPathLight : settings.wallpaperPathDark;
auto style = shouldBeLight ? settings.wallpaperStyleLight : settings.wallpaperStyleDark;
if (auto e = SetDesktopWallpaper(wallpaperPath, style, settings.wallpaperVirtualDesktop) == 0)
{
Logger::info(L"[LightSwitchService] Wallpaper is changed to {}.", wallpaperPath);
}
else
{
Logger::error(L"[LightSwitchService] Failed to set wallpaper, error: {}.", e);
}
}
};
void ApplyTheme(bool shouldBeLight, bool changeWallpaper)
{ {
const auto& s = LightSwitchSettings::settings(); const auto& s = LightSwitchSettings::settings();
bool somethingChanged = false;
if (s.changeSystem) if (s.changeSystem)
{ {
@@ -135,6 +156,7 @@ void ApplyTheme(bool shouldBeLight)
{ {
SetSystemTheme(shouldBeLight); SetSystemTheme(shouldBeLight);
Logger::info(L"[LightSwitchService] Changed system theme to {}.", shouldBeLight ? L"light" : L"dark"); Logger::info(L"[LightSwitchService] Changed system theme to {}.", shouldBeLight ? L"light" : L"dark");
somethingChanged = true;
} }
} }
@@ -145,6 +167,15 @@ void ApplyTheme(bool shouldBeLight)
{ {
SetAppsTheme(shouldBeLight); SetAppsTheme(shouldBeLight);
Logger::info(L"[LightSwitchService] Changed apps theme to {}.", shouldBeLight ? L"light" : L"dark"); Logger::info(L"[LightSwitchService] Changed apps theme to {}.", shouldBeLight ? L"light" : L"dark");
somethingChanged = true;
}
}
if (somethingChanged)
{
if (changeWallpaper)
{
SetWallpaper(shouldBeLight);
} }
} }
} }
@@ -174,7 +205,7 @@ static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateMan
if (s.scheduleMode == ScheduleMode::FollowNightLight) if (s.scheduleMode == ScheduleMode::FollowNightLight)
{ {
shouldBeLight = !IsNightLightEnabled(); shouldBeLight = !IsNightLightEnabled();
} }
else else
{ {
shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark); shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
@@ -357,6 +388,8 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{ {
Trace::LightSwitch::RegisterProvider();
if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
{ {
wchar_t msg[160]; wchar_t msg[160];
@@ -364,12 +397,14 @@ int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
msg, msg,
L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator."); L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
Logger::info(msg); Logger::info(msg);
Trace::LightSwitch::UnregisterProvider();
return 0; return 0;
} }
int argc = 0; int argc = 0;
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
int rc = _tmain(argc, argv); // reuse your existing logic int rc = _tmain(argc, argv); // reuse your existing logic
LocalFree(argv); LocalFree(argv);
Trace::LightSwitch::UnregisterProvider();
return rc; return rc;
} }

View File

@@ -53,18 +53,7 @@
<ConformanceMode>true</ConformanceMode> <ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader> <PrecompiledHeader>NotUsing</PrecompiledHeader>
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories> <AdditionalIncludeDirectories>..\LightSwitchCommon;.\..\;..\..\..\common;..\..\..\common\logger;..\..\..\common\utils;..\..\..\common\SettingsAPI;..\..\..\common\Telemetry;..\..\..\;..\..\..\..\deps\spdlog\include;./;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
./../;
..\..\..\common;
..\..\..\common\logger;
..\..\..\common\utils;
..\..\..\common\SettingsAPI;
..\..\..\common\Telemetry;
..\..\..\;
..\..\..\..\deps\spdlog\include;
./;
%(AdditionalIncludeDirectories)
</AdditionalIncludeDirectories>
</ClCompile> </ClCompile>
<Link> <Link>
<SubSystem>Windows</SubSystem> <SubSystem>Windows</SubSystem>
@@ -77,9 +66,9 @@
<ClCompile Include="LightSwitchSettings.cpp" /> <ClCompile Include="LightSwitchSettings.cpp" />
<ClCompile Include="LightSwitchStateManager.cpp" /> <ClCompile Include="LightSwitchStateManager.cpp" />
<ClCompile Include="NightLightRegistryObserver.cpp" /> <ClCompile Include="NightLightRegistryObserver.cpp" />
<ClCompile Include="SettingsConstants.cpp" /> <ClCompile Include="..\LightSwitchCommon\ThemeHelper.cpp" />
<ClCompile Include="ThemeHelper.cpp" />
<ClCompile Include="ThemeScheduler.cpp" /> <ClCompile Include="ThemeScheduler.cpp" />
<ClCompile Include="trace.cpp" />
<ClCompile Include="WinHookEventIDs.cpp" /> <ClCompile Include="WinHookEventIDs.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -90,10 +79,11 @@
<ClInclude Include="LightSwitchStateManager.h" /> <ClInclude Include="LightSwitchStateManager.h" />
<ClInclude Include="LightSwitchUtils.h" /> <ClInclude Include="LightSwitchUtils.h" />
<ClInclude Include="NightLightRegistryObserver.h" /> <ClInclude Include="NightLightRegistryObserver.h" />
<ClInclude Include="SettingsConstants.h" />
<ClInclude Include="SettingsObserver.h" /> <ClInclude Include="SettingsObserver.h" />
<ClInclude Include="ThemeHelper.h" /> <ClInclude Include="..\LightSwitchCommon\ThemeHelper.h" />
<ClInclude Include="..\LightSwitchCommon\SettingsConstants.h" />
<ClInclude Include="ThemeScheduler.h" /> <ClInclude Include="ThemeScheduler.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="WinHookEventIDs.h" /> <ClInclude Include="WinHookEventIDs.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -21,15 +21,12 @@
<ClCompile Include="ThemeScheduler.cpp"> <ClCompile Include="ThemeScheduler.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="ThemeHelper.cpp"> <ClCompile Include="..\LightSwitchCommon\ThemeHelper.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="LightSwitchSettings.cpp"> <ClCompile Include="LightSwitchSettings.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="SettingsConstants.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WinHookEventIDs.cpp"> <ClCompile Include="WinHookEventIDs.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
@@ -39,6 +36,9 @@
<ClCompile Include="NightLightRegistryObserver.cpp"> <ClCompile Include="NightLightRegistryObserver.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="ThemeScheduler.h"> <ClInclude Include="ThemeScheduler.h">
@@ -47,10 +47,10 @@
<ClInclude Include="ThemeHelper.h"> <ClInclude Include="ThemeHelper.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="LightSwitchSettings.h"> <ClInclude Include="..\LightSwitchCommon\LightSwitchSettings.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="SettingsConstants.h"> <ClInclude Include="..\LightSwitchCommon\SettingsConstants.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="SettingsObserver.h"> <ClInclude Include="SettingsObserver.h">
@@ -68,6 +68,9 @@
<ClInclude Include="NightLightRegistryObserver.h"> <ClInclude Include="NightLightRegistryObserver.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" /> <Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />

View File

@@ -5,6 +5,7 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <logger.h> #include <logger.h>
#include <LightSwitchService/trace.h>
using namespace std; using namespace std;
@@ -151,6 +152,7 @@ void LightSwitchSettings::LoadSettings()
if (m_settings.scheduleMode != newMode) if (m_settings.scheduleMode != newMode)
{ {
m_settings.scheduleMode = newMode; m_settings.scheduleMode = newMode;
Trace::LightSwitch::ScheduleModeToggled(val);
NotifyObservers(SettingId::ScheduleMode); NotifyObservers(SettingId::ScheduleMode);
} }
} }
@@ -220,6 +222,8 @@ void LightSwitchSettings::LoadSettings()
} }
} }
bool themeTargetChanged = false;
// ChangeSystem // ChangeSystem
if (const auto jsonVal = values.get_bool_value(L"changeSystem")) if (const auto jsonVal = values.get_bool_value(L"changeSystem"))
{ {
@@ -227,6 +231,7 @@ void LightSwitchSettings::LoadSettings()
if (m_settings.changeSystem != val) if (m_settings.changeSystem != val)
{ {
m_settings.changeSystem = val; m_settings.changeSystem = val;
themeTargetChanged = true;
NotifyObservers(SettingId::ChangeSystem); NotifyObservers(SettingId::ChangeSystem);
} }
} }
@@ -238,9 +243,76 @@ void LightSwitchSettings::LoadSettings()
if (m_settings.changeApps != val) if (m_settings.changeApps != val)
{ {
m_settings.changeApps = val; m_settings.changeApps = val;
themeTargetChanged = true;
NotifyObservers(SettingId::ChangeApps); NotifyObservers(SettingId::ChangeApps);
} }
} }
// For ChangeSystem/ChangeApps changes, log telemetry
if (themeTargetChanged)
{
Trace::LightSwitch::ThemeTargetChanged(m_settings.changeApps, m_settings.changeSystem);
}
if (const auto jsonVal = values.get_bool_value(L"wallpaperEnabled"))
{
auto val = *jsonVal;
if (m_settings.wallpaperEnabled != val)
{
m_settings.wallpaperEnabled = val;
NotifyObservers(SettingId::WallpaperEnabled);
}
}
if (const auto jsonVal = values.get_bool_value(L"wallpaperVirtualDesktopEnabled"))
{
auto val = *jsonVal;
if (m_settings.wallpaperVirtualDesktop != val)
{
m_settings.wallpaperVirtualDesktop = val;
NotifyObservers(SettingId::WallpaperVirtualDesktopEnabled);
}
}
if (const auto jsonVal = values.get_int_value(L"wallpaperStyleLight"))
{
auto val = *jsonVal;
if (m_settings.wallpaperStyleLight != val)
{
m_settings.wallpaperStyleLight = val;
NotifyObservers(SettingId::WallpaperStyleLight);
}
}
if (const auto jsonVal = values.get_int_value(L"wallpaperStyleDark"))
{
auto val = *jsonVal;
if (m_settings.wallpaperStyleDark != val)
{
m_settings.wallpaperStyleDark = val;
NotifyObservers(SettingId::WallpaperStyleDark);
}
}
if (const auto jsonVal = values.get_string_value(L"wallpaperPathLight"))
{
auto val = *jsonVal;
if (m_settings.wallpaperPathLight != val)
{
m_settings.wallpaperPathLight = val;
NotifyObservers(SettingId::WallpaperPathLight);
}
}
if (const auto jsonVal = values.get_string_value(L"wallpaperPathDark"))
{
auto val = *jsonVal;
if (m_settings.wallpaperPathDark != val)
{
m_settings.wallpaperPathDark = val;
NotifyObservers(SettingId::WallpaperPathDark);
}
}
} }
catch (...) catch (...)
{ {

View File

@@ -67,6 +67,13 @@ struct LightSwitchConfig
bool changeSystem = false; bool changeSystem = false;
bool changeApps = false; bool changeApps = false;
bool wallpaperEnabled = false;
bool wallpaperVirtualDesktop = false;
int wallpaperStyleLight = 0;
int wallpaperStyleDark = 0;
std::wstring wallpaperPathLight;
std::wstring wallpaperPathDark;
}; };
class LightSwitchSettings class LightSwitchSettings

View File

@@ -4,8 +4,9 @@
#include <LightSwitchUtils.h> #include <LightSwitchUtils.h>
#include "ThemeScheduler.h" #include "ThemeScheduler.h"
#include <ThemeHelper.h> #include <ThemeHelper.h>
#include <filesystem>
void ApplyTheme(bool shouldBeLight); void ApplyTheme(bool shouldBeLight, bool changeWallpaper);
// Constructor // Constructor
LightSwitchStateManager::LightSwitchStateManager() LightSwitchStateManager::LightSwitchStateManager()
@@ -147,6 +148,11 @@ static std::pair<int, int> update_sun_times(auto& settings)
return { newLightTime, newDarkTime }; return { newLightTime, newDarkTime };
} }
static bool IsValidPath(const std::wstring& path)
{
return !path.empty() && std::filesystem::exists(path);
}
// Internal: decide what should happen now // Internal: decide what should happen now
void LightSwitchStateManager::EvaluateAndApplyIfNeeded() void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
{ {
@@ -240,6 +246,11 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight); bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight); bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
bool changeWallpaper =
_currentSettings.wallpaperEnabled &&
IsValidPath(_currentSettings.wallpaperPathDark) &&
IsValidPath(_currentSettings.wallpaperPathLight);
/* Logger::debug( /* Logger::debug(
L"[LightSwitchStateManager] now = {:02d}:{:02d}, light boundary = {:02d}:{:02d} ({}), dark boundary = {:02d}:{:02d} ({})", L"[LightSwitchStateManager] now = {:02d}:{:02d}, light boundary = {:02d}:{:02d} ({}), dark boundary = {:02d}:{:02d} ({})",
now / 60, now / 60,
@@ -260,11 +271,11 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange)) if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange))
{ {
Logger::info(L"[LightSwitchStateManager] Applying {} theme", shouldBeLight ? L"light" : L"dark"); Logger::info(L"[LightSwitchStateManager] Applying {} theme", shouldBeLight ? L"light" : L"dark");
ApplyTheme(shouldBeLight); ApplyTheme(shouldBeLight, changeWallpaper);
_state.isSystemLightActive = GetCurrentSystemTheme(); _state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme(); _state.isAppsLightActive = GetCurrentAppsTheme();
} }
_state.lastTickMinutes = now; _state.lastTickMinutes = now;
} }

View File

@@ -1 +0,0 @@
#include "SettingsConstants.h"

View File

@@ -1,17 +0,0 @@
#pragma once
enum class SettingId
{
ScheduleMode = 0,
Latitude,
Longitude,
LightTime,
DarkTime,
Sunrise_Offset,
Sunset_Offset,
ChangeSystem,
ChangeApps
};
constexpr wchar_t PERSONALIZATION_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr wchar_t NIGHT_LIGHT_REGISTRY_PATH[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\CloudStore\\Store\\DefaultAccount\\Current\\default$windows.data.bluelightreduction.bluelightreductionstate\\windows.data.bluelightreduction.bluelightreductionstate";

View File

@@ -1,139 +0,0 @@
#include <windows.h>
#include <logger/logger_settings.h>
#include <logger/logger.h>
#include <utils/logger_helper.h>
#include "ThemeHelper.h"
#include <SettingsConstants.h>
// Controls changing the themes.
static void ResetColorPrevalence()
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = 0; // back to default value
RegSetValueEx(hKey, L"ColorPrevalence", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_DWMCOLORIZATIONCOLORCHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetAppsTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"AppsUseLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
void SetSystemTheme(bool mode)
{
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER,
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_SET_VALUE,
&hKey) == ERROR_SUCCESS)
{
DWORD value = mode;
RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(hKey);
if (mode) // if are changing to light mode
{
ResetColorPrevalence();
Logger::info(L"[LightSwitchService] Reset ColorPrevalence to default when switching to light mode.");
}
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast<LPARAM>(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
}
}
// Can think of this as "is the current theme light?"
bool GetCurrentSystemTheme()
{
HKEY hKey;
DWORD value = 1; // default = light
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"SystemUsesLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
}
return value == 1; // true = light, false = dark
}
bool GetCurrentAppsTheme()
{
HKEY hKey;
DWORD value = 1;
DWORD size = sizeof(value);
if (RegOpenKeyEx(HKEY_CURRENT_USER,
PERSONALIZATION_REGISTRY_PATH,
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
RegQueryValueEx(hKey, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
RegCloseKey(hKey);
}
return value == 1; // true = light, false = dark
}
bool IsNightLightEnabled()
{
HKEY hKey;
const wchar_t* path = NIGHT_LIGHT_REGISTRY_PATH;
if (RegOpenKeyExW(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKey) != ERROR_SUCCESS)
return false;
// RegGetValueW will set size to the size of the data and we expect that to be at least 25 bytes (we need to access bytes 23 and 24)
DWORD size = 0;
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, nullptr, &size) != ERROR_SUCCESS || size < 25)
{
RegCloseKey(hKey);
return false;
}
std::vector<BYTE> data(size);
if (RegGetValueW(hKey, nullptr, L"Data", RRF_RT_REG_BINARY, nullptr, data.data(), &size) != ERROR_SUCCESS)
{
RegCloseKey(hKey);
return false;
}
RegCloseKey(hKey);
return data[23] == 0x10 && data[24] == 0x00;
}

View File

@@ -1,6 +0,0 @@
#pragma once
void SetSystemTheme(bool dark);
void SetAppsTheme(bool dark);
bool GetCurrentSystemTheme();
bool GetCurrentAppsTheme();
bool IsNightLightEnabled();

View File

@@ -0,0 +1,43 @@
#include "pch.h"
#include "trace.h"
// Telemetry strings should not be localized.
#define LoggingProviderKey "Microsoft.PowerToys"
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
LoggingProviderKey,
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::LightSwitch::RegisterProvider()
{
TraceLoggingRegister(g_hProvider);
}
void Trace::LightSwitch::UnregisterProvider()
{
TraceLoggingUnregister(g_hProvider);
}
void Trace::LightSwitch::ScheduleModeToggled(const std::wstring& newMode) noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"LightSwitch_ScheduleModeToggled",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingWideString(newMode.c_str(), "NewMode"));
}
void Trace::LightSwitch::ThemeTargetChanged(bool changeApps, bool changeSystem) noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"LightSwitch_ThemeTargetChanged",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(changeApps, "ChangeApps"),
TraceLoggingBoolean(changeSystem, "ChangeSystem"));
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include <common/Telemetry/TraceBase.h>
#include <string>
class Trace
{
public:
class LightSwitch : public telemetry::TraceBase
{
public:
static void RegisterProvider();
static void UnregisterProvider();
static void ScheduleModeToggled(const std::wstring& newMode) noexcept;
static void ThemeTargetChanged(bool changeApps, bool changeSystem) noexcept;
};
};

View File

@@ -77,10 +77,8 @@ protected:
int m_sonarRadius = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS; int m_sonarRadius = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS;
int m_sonarZoomFactor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM; int m_sonarZoomFactor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM;
DWORD m_fadeDuration = FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS; DWORD m_fadeDuration = FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS;
int m_finalAlphaNumerator = 100; // legacy (root now always animates to 1.0; kept for GDI fallback compatibility)
std::vector<std::wstring> m_excludedApps; std::vector<std::wstring> m_excludedApps;
int m_shakeMinimumDistance = FIND_MY_MOUSE_DEFAULT_SHAKE_MINIMUM_DISTANCE; int m_shakeMinimumDistance = FIND_MY_MOUSE_DEFAULT_SHAKE_MINIMUM_DISTANCE;
static constexpr int FinalAlphaDenominator = 100;
winrt::Microsoft::UI::Dispatching::DispatcherQueueController m_dispatcherQueueController{ nullptr }; winrt::Microsoft::UI::Dispatching::DispatcherQueueController m_dispatcherQueueController{ nullptr };
// Don't consider movements started past these milliseconds to detect shaking. // Don't consider movements started past these milliseconds to detect shaking.
@@ -155,7 +153,7 @@ private:
void DetectShake(); void DetectShake();
bool KeyboardInputCanActivate(); bool KeyboardInputCanActivate();
void StartSonar(); void StartSonar(FindMyMouseActivationMethod activationMethod);
void StopSonar(); void StopSonar();
}; };
@@ -275,7 +273,7 @@ LRESULT SuperSonar<D>::BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) n
{ {
if (m_sonarStart == NoSonar) if (m_sonarStart == NoSonar)
{ {
StartSonar(); StartSonar(FindMyMouseActivationMethod::Shortcut);
} }
else else
{ {
@@ -384,7 +382,7 @@ void SuperSonar<D>::OnSonarKeyboardInput(RAWINPUT const& input)
IsEqual(m_lastKeyPos, ptCursor)) IsEqual(m_lastKeyPos, ptCursor))
{ {
m_sonarState = SonarState::ControlDown2; m_sonarState = SonarState::ControlDown2;
StartSonar(); StartSonar(m_activationMethod);
} }
else else
{ {
@@ -451,7 +449,7 @@ void SuperSonar<D>::DetectShake()
if (diagonal > 0 && distanceTravelled / diagonal > (m_shakeFactor / 100.f)) if (diagonal > 0 && distanceTravelled / diagonal > (m_shakeFactor / 100.f))
{ {
m_movementHistory.clear(); m_movementHistory.clear();
StartSonar(); StartSonar(m_activationMethod);
} }
} }
@@ -519,7 +517,7 @@ void SuperSonar<D>::OnSonarMouseInput(RAWINPUT const& input)
} }
template<typename D> template<typename D>
void SuperSonar<D>::StartSonar() void SuperSonar<D>::StartSonar(FindMyMouseActivationMethod activationMethod)
{ {
// Don't activate if game mode is on. // Don't activate if game mode is on.
if (m_doNotActivateOnGameMode && detect_game_mode()) if (m_doNotActivateOnGameMode && detect_game_mode())
@@ -532,7 +530,7 @@ void SuperSonar<D>::StartSonar()
return; return;
} }
Trace::MousePointerFocused(); Trace::MousePointerFocused(static_cast<int>(activationMethod));
// Cover the entire virtual screen. // Cover the entire virtual screen.
// HACK: Draw with 1 pixel off. Otherwise, Windows glitches the task bar transparency when a transparent window fill the whole screen. // HACK: Draw with 1 pixel off. Otherwise, Windows glitches the task bar transparency when a transparent window fill the whole screen.
SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN) + 1, GetSystemMetrics(SM_YVIRTUALSCREEN) + 1, GetSystemMetrics(SM_CXVIRTUALSCREEN) - 2, GetSystemMetrics(SM_CYVIRTUALSCREEN) - 2, 0); SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN) + 1, GetSystemMetrics(SM_YVIRTUALSCREEN) + 1, GetSystemMetrics(SM_CXVIRTUALSCREEN) - 2, GetSystemMetrics(SM_CYVIRTUALSCREEN) - 2, 0);
@@ -816,13 +814,16 @@ private:
// Dim color (source) // Dim color (source)
m_dimColorBrush = m_compositor.CreateColorBrush(m_backgroundColor); m_dimColorBrush = m_compositor.CreateColorBrush(m_backgroundColor);
// Radial gradient mask (center transparent, outer opaque) // Radial gradient mask (center transparent, outer opaque)
// Fixed feather width: 1px for radius < 300, 2px for radius >= 300
const float featherPixels = (m_sonarRadius >= 300) ? 2.0f : 1.0f;
const float featherOffset = 1.0f - featherPixels / (rDip * zoom);
m_spotlightMaskGradient = m_compositor.CreateRadialGradientBrush(); m_spotlightMaskGradient = m_compositor.CreateRadialGradientBrush();
m_spotlightMaskGradient.MappingMode(muxc::CompositionMappingMode::Absolute); m_spotlightMaskGradient.MappingMode(muxc::CompositionMappingMode::Absolute);
m_maskStopCenter = m_compositor.CreateColorGradientStop(); m_maskStopCenter = m_compositor.CreateColorGradientStop();
m_maskStopCenter.Offset(0.0f); m_maskStopCenter.Offset(0.0f);
m_maskStopCenter.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0)); m_maskStopCenter.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
m_maskStopInner = m_compositor.CreateColorGradientStop(); m_maskStopInner = m_compositor.CreateColorGradientStop();
m_maskStopInner.Offset(0.995f); m_maskStopInner.Offset(featherOffset);
m_maskStopInner.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0)); m_maskStopInner.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0));
m_maskStopOuter = m_compositor.CreateColorGradientStop(); m_maskStopOuter = m_compositor.CreateColorGradientStop();
m_maskStopOuter.Offset(1.0f); m_maskStopOuter.Offset(1.0f);
@@ -852,23 +853,7 @@ private:
m_root.ImplicitAnimations(collection); m_root.ImplicitAnimations(collection);
// 6) Spotlight radius shrinks as opacity increases (expression animation) // 6) Spotlight radius shrinks as opacity increases (expression animation)
auto radiusExpression = m_compositor.CreateExpressionAnimation(); SetupRadiusAnimations(rDip * zoom, rDip, featherPixels);
radiusExpression.SetReferenceParameter(L"Root", m_root);
wchar_t expressionText[256];
winrt::check_hresult(StringCchPrintfW(
expressionText, ARRAYSIZE(expressionText), L"Lerp(Vector2(%d, %d), Vector2(%d, %d), Root.Opacity)", m_sonarRadius * m_sonarZoomFactor, m_sonarRadius * m_sonarZoomFactor, m_sonarRadius, m_sonarRadius));
radiusExpression.Expression(expressionText);
m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", radiusExpression);
// Also animate spotlight geometry radius for visual consistency
if (m_circleGeometry)
{
auto radiusExpression2 = m_compositor.CreateExpressionAnimation();
radiusExpression2.SetReferenceParameter(L"Root", m_root);
radiusExpression2.Expression(expressionText);
m_circleGeometry.StartAnimation(L"Radius", radiusExpression2);
}
// Composition created successfully // Composition created successfully
return true; return true;
@@ -887,6 +872,41 @@ private:
} }
} }
// Helper to setup radius and feather expression animations
void SetupRadiusAnimations(float startRadiusDip, float endRadiusDip, float featherPixels)
{
// Radius expression: shrinks from startRadiusDip to endRadiusDip as opacity goes 0->1
auto radiusExpression = m_compositor.CreateExpressionAnimation();
radiusExpression.SetReferenceParameter(L"Root", m_root);
wchar_t expressionText[256];
winrt::check_hresult(StringCchPrintfW(
expressionText, ARRAYSIZE(expressionText),
L"Lerp(Vector2(%.1f, %.1f), Vector2(%.1f, %.1f), Root.Opacity)",
startRadiusDip, startRadiusDip, endRadiusDip, endRadiusDip));
radiusExpression.Expression(expressionText);
m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", radiusExpression);
// Feather expression: maintains fixed pixel width as radius changes
auto featherExpression = m_compositor.CreateExpressionAnimation();
featherExpression.SetReferenceParameter(L"Root", m_root);
wchar_t featherExpressionText[256];
winrt::check_hresult(StringCchPrintfW(
featherExpressionText, ARRAYSIZE(featherExpressionText),
L"1.0f - %.1ff / Lerp(%.1ff, %.1ff, Root.Opacity)",
featherPixels, startRadiusDip, endRadiusDip));
featherExpression.Expression(featherExpressionText);
m_maskStopInner.StartAnimation(L"Offset", featherExpression);
// Circle geometry radius for visual consistency
if (m_circleGeometry)
{
auto radiusExpression2 = m_compositor.CreateExpressionAnimation();
radiusExpression2.SetReferenceParameter(L"Root", m_root);
radiusExpression2.Expression(expressionText);
m_circleGeometry.StartAnimation(L"Radius", radiusExpression2);
}
}
void UpdateIslandSize() void UpdateIslandSize()
{ {
if (!m_island) if (!m_island)
@@ -964,27 +984,21 @@ public:
const float scale = static_cast<float>(m_surface.XamlRoot().RasterizationScale()); const float scale = static_cast<float>(m_surface.XamlRoot().RasterizationScale());
const float rDip = m_sonarRadiusFloat / scale; const float rDip = m_sonarRadiusFloat / scale;
const float zoom = static_cast<float>(m_sonarZoomFactor); const float zoom = static_cast<float>(m_sonarZoomFactor);
const float featherPixels = (m_sonarRadius >= 300) ? 2.0f : 1.0f;
const float startRadiusDip = rDip * zoom;
m_spotlightMaskGradient.StopAnimation(L"EllipseRadius"); m_spotlightMaskGradient.StopAnimation(L"EllipseRadius");
m_spotlightMaskGradient.EllipseCenter({ rDip * zoom, rDip * zoom }); m_maskStopInner.StopAnimation(L"Offset");
if (m_spotlight)
{
m_spotlight.Size({ rDip * 2 * zoom, rDip * 2 * zoom });
m_circleShape.Offset({ rDip * zoom, rDip * zoom });
}
auto radiusExpression = m_compositor.CreateExpressionAnimation();
radiusExpression.SetReferenceParameter(L"Root", m_root);
wchar_t expressionText[256];
winrt::check_hresult(StringCchPrintfW(expressionText, ARRAYSIZE(expressionText), L"Lerp(Vector2(%d, %d), Vector2(%d, %d), Root.Opacity)", m_sonarRadius * m_sonarZoomFactor, m_sonarRadius * m_sonarZoomFactor, m_sonarRadius, m_sonarRadius));
radiusExpression.Expression(expressionText);
m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", radiusExpression);
if (m_circleGeometry) if (m_circleGeometry)
{ {
m_circleGeometry.StopAnimation(L"Radius"); m_circleGeometry.StopAnimation(L"Radius");
auto radiusExpression2 = m_compositor.CreateExpressionAnimation();
radiusExpression2.SetReferenceParameter(L"Root", m_root);
radiusExpression2.Expression(expressionText);
m_circleGeometry.StartAnimation(L"Radius", radiusExpression2);
} }
m_spotlightMaskGradient.EllipseCenter({ startRadiusDip, startRadiusDip });
if (m_spotlight)
{
m_spotlight.Size({ rDip * 2 * zoom, rDip * 2 * zoom });
m_circleShape.Offset({ startRadiusDip, startRadiusDip });
}
SetupRadiusAnimations(startRadiusDip, rDip, featherPixels);
} }
}); });
if (!enqueueSucceeded) if (!enqueueSucceeded)
@@ -1018,202 +1032,6 @@ private:
muxc::ScalarKeyFrameAnimation m_animation{ nullptr }; muxc::ScalarKeyFrameAnimation m_animation{ nullptr };
}; };
template<typename D>
struct GdiSonar : SuperSonar<D>
{
LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam) noexcept
{
switch (message)
{
case WM_CREATE:
SetLayeredWindowAttributes(this->m_hwnd, 0, 0, LWA_ALPHA);
break;
case WM_TIMER:
switch (wParam)
{
case TIMER_ID_FADE:
OnFadeTimer();
break;
}
break;
case WM_PAINT:
this->Shim()->OnPaint();
break;
}
return this->BaseWndProc(message, wParam, lParam);
}
void BeforeMoveSonar() { this->Shim()->InvalidateSonar(); }
void AfterMoveSonar() { this->Shim()->InvalidateSonar(); }
void SetSonarVisibility(bool visible)
{
m_alphaTarget = visible ? MaxAlpha : 0;
m_fadeStart = GetTickCount() - FadeFramePeriod;
SetTimer(this->m_hwnd, TIMER_ID_FADE, FadeFramePeriod, nullptr);
OnFadeTimer();
}
void OnFadeTimer()
{
auto now = GetTickCount();
auto step = (int)((now - m_fadeStart) * MaxAlpha / this->m_fadeDuration);
this->Shim()->InvalidateSonar();
if (m_alpha < m_alphaTarget)
{
m_alpha += step;
if (m_alpha > m_alphaTarget)
m_alpha = m_alphaTarget;
}
else if (m_alpha > m_alphaTarget)
{
m_alpha -= step;
if (m_alpha < m_alphaTarget)
m_alpha = m_alphaTarget;
}
SetLayeredWindowAttributes(this->m_hwnd, 0, (BYTE)m_alpha, LWA_ALPHA);
this->Shim()->InvalidateSonar();
if (m_alpha == m_alphaTarget)
{
KillTimer(this->m_hwnd, TIMER_ID_FADE);
if (m_alpha == 0)
{
ShowWindow(this->m_hwnd, SW_HIDE);
}
}
else
{
ShowWindow(this->m_hwnd, SW_SHOWNOACTIVATE);
}
}
protected:
int CurrentSonarRadius()
{
int range = MaxAlpha - m_alpha;
int radius = this->m_sonarRadius + this->m_sonarRadius * range * (this->m_sonarZoomFactor - 1) / MaxAlpha;
return radius;
}
private:
static constexpr DWORD FadeFramePeriod = 10;
int MaxAlpha = SuperSonar<D>::m_finalAlphaNumerator * 255 / SuperSonar<D>::FinalAlphaDenominator;
static constexpr DWORD TIMER_ID_FADE = 101;
private:
int m_alpha = 0;
int m_alphaTarget = 0;
DWORD m_fadeStart = 0;
};
struct GdiSpotlight : GdiSonar<GdiSpotlight>
{
void InvalidateSonar()
{
RECT rc;
auto radius = CurrentSonarRadius();
rc.left = this->m_sonarPos.x - radius;
rc.top = this->m_sonarPos.y - radius;
rc.right = this->m_sonarPos.x + radius;
rc.bottom = this->m_sonarPos.y + radius;
InvalidateRect(this->m_hwnd, &rc, FALSE);
}
void OnPaint()
{
PAINTSTRUCT ps;
BeginPaint(this->m_hwnd, &ps);
auto radius = CurrentSonarRadius();
auto spotlight = CreateRoundRectRgn(
this->m_sonarPos.x - radius, this->m_sonarPos.y - radius, this->m_sonarPos.x + radius, this->m_sonarPos.y + radius, radius * 2, radius * 2);
FillRgn(ps.hdc, spotlight, static_cast<HBRUSH>(GetStockObject(WHITE_BRUSH)));
Sleep(1000 / 60);
ExtSelectClipRgn(ps.hdc, spotlight, RGN_DIFF);
FillRect(ps.hdc, &ps.rcPaint, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));
DeleteObject(spotlight);
EndPaint(this->m_hwnd, &ps);
}
};
struct GdiCrosshairs : GdiSonar<GdiCrosshairs>
{
void InvalidateSonar()
{
RECT rc;
auto radius = CurrentSonarRadius();
GetClientRect(m_hwnd, &rc);
rc.left = m_sonarPos.x - radius;
rc.right = m_sonarPos.x + radius;
InvalidateRect(m_hwnd, &rc, FALSE);
GetClientRect(m_hwnd, &rc);
rc.top = m_sonarPos.y - radius;
rc.bottom = m_sonarPos.y + radius;
InvalidateRect(m_hwnd, &rc, FALSE);
}
void OnPaint()
{
PAINTSTRUCT ps;
BeginPaint(this->m_hwnd, &ps);
auto radius = CurrentSonarRadius();
RECT rc;
HBRUSH white = static_cast<HBRUSH>(GetStockObject(WHITE_BRUSH));
rc.left = m_sonarPos.x - radius;
rc.top = ps.rcPaint.top;
rc.right = m_sonarPos.x + radius;
rc.bottom = ps.rcPaint.bottom;
FillRect(ps.hdc, &rc, white);
rc.left = ps.rcPaint.left;
rc.top = m_sonarPos.y - radius;
rc.right = ps.rcPaint.right;
rc.bottom = m_sonarPos.y + radius;
FillRect(ps.hdc, &rc, white);
HBRUSH black = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
// Top left
rc.left = ps.rcPaint.left;
rc.top = ps.rcPaint.top;
rc.right = m_sonarPos.x - radius;
rc.bottom = m_sonarPos.y - radius;
FillRect(ps.hdc, &rc, black);
// Top right
rc.left = m_sonarPos.x + radius;
rc.top = ps.rcPaint.top;
rc.right = ps.rcPaint.right;
rc.bottom = m_sonarPos.y - radius;
FillRect(ps.hdc, &rc, black);
// Bottom left
rc.left = ps.rcPaint.left;
rc.top = m_sonarPos.y + radius;
rc.right = m_sonarPos.x - radius;
rc.bottom = ps.rcPaint.bottom;
FillRect(ps.hdc, &rc, black);
// Bottom right
rc.left = m_sonarPos.x + radius;
rc.top = m_sonarPos.y + radius;
rc.right = ps.rcPaint.right;
rc.bottom = ps.rcPaint.bottom;
FillRect(ps.hdc, &rc, black);
EndPaint(this->m_hwnd, &ps);
}
};
#pragma endregion Super_Sonar_Base_Code #pragma endregion Super_Sonar_Base_Code
#pragma region Super_Sonar_API #pragma region Super_Sonar_API
@@ -1284,4 +1102,4 @@ HWND GetSonarHwnd() noexcept
return nullptr; return nullptr;
} }
#pragma endregion Super_Sonar_API #pragma endregion Super_Sonar_API

View File

@@ -154,4 +154,4 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" /> <Import Project="..\..\..\..\deps\spdlog.props" />
</Project> </Project>

View File

@@ -22,11 +22,12 @@ void Trace::EnableFindMyMouse(const bool enabled) noexcept
} }
// Log that the user activated the module by focusing the mouse pointer // Log that the user activated the module by focusing the mouse pointer
void Trace::MousePointerFocused() noexcept void Trace::MousePointerFocused(const int activationMethod) noexcept
{ {
TraceLoggingWriteWrapper( TraceLoggingWriteWrapper(
g_hProvider, g_hProvider,
"FindMyMouse_MousePointerFocused", "FindMyMouse_MousePointerFocused",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingInt32(activationMethod, "ActivationMethod"));
} }

View File

@@ -9,5 +9,6 @@ public:
static void EnableFindMyMouse(const bool enabled) noexcept; static void EnableFindMyMouse(const bool enabled) noexcept;
// Log that the user activated the module by focusing the mouse pointer // Log that the user activated the module by focusing the mouse pointer
static void MousePointerFocused() noexcept; // activationMethod: 0 = DoubleLeftControlKey, 1 = DoubleRightControlKey, 2 = ShakeMouse, 3 = Shortcut
static void MousePointerFocused(const int activationMethod) noexcept;
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -18,12 +18,12 @@ using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using Microsoft.VisualStudio.Threading; using Microsoft.VisualStudio.Threading;
using MouseWithoutBorders.Core;
using Newtonsoft.Json; using Newtonsoft.Json;
using StreamJsonRpc; using StreamJsonRpc;
#if !MM_HELPER #if !MM_HELPER
using MouseWithoutBorders.Class; using MouseWithoutBorders.Class;
using MouseWithoutBorders.Core;
#endif #endif
using SystemClipboard = System.Windows.Forms.Clipboard; using SystemClipboard = System.Windows.Forms.Clipboard;
@@ -246,11 +246,11 @@ WellKnownSidType.AuthenticatedUserSid, null);
CancellationToken cancellationToken = _serverTaskCancellationSource.Token; CancellationToken cancellationToken = _serverTaskCancellationSource.Token;
IpcChannel<ClipboardHelper>.StartIpcServer(ChannelName + "/" + RemoteObjectName, cancellationToken); IpcChannel<ClipboardHelper>.StartIpcServer(ChannelName + "/" + RemoteObjectName, cancellationToken);
Common.IpcChannelCreated = true; IpcChannelHelper.IpcChannelCreated = true;
} }
catch (Exception e) catch (Exception e)
{ {
Common.IpcChannelCreated = false; IpcChannelHelper.IpcChannelCreated = false;
Common.ShowToolTip("Error setting up clipboard sharing, clipboard sharing will not work!", 5000, ToolTipIcon.Error); Common.ShowToolTip("Error setting up clipboard sharing, clipboard sharing will not work!", 5000, ToolTipIcon.Error);
Logger.Log(e); Logger.Log(e);
} }
@@ -405,7 +405,7 @@ WellKnownSidType.AuthenticatedUserSid, null);
try try
{ {
rv = Common.Retry(nameof(SystemClipboard.ContainsFileDropList), () => { return SystemClipboard.ContainsFileDropList(); }, (log) => Log(log)); rv = IpcChannelHelper.Retry(nameof(SystemClipboard.ContainsFileDropList), () => { return SystemClipboard.ContainsFileDropList(); }, (log) => Log(log));
} }
catch (ExternalException e) catch (ExternalException e)
{ {
@@ -427,7 +427,7 @@ WellKnownSidType.AuthenticatedUserSid, null);
try try
{ {
rv = Common.Retry(nameof(SystemClipboard.ContainsImage), () => { return SystemClipboard.ContainsImage(); }, (log) => Log(log)); rv = IpcChannelHelper.Retry(nameof(SystemClipboard.ContainsImage), () => { return SystemClipboard.ContainsImage(); }, (log) => Log(log));
} }
catch (ExternalException e) catch (ExternalException e)
{ {
@@ -449,7 +449,7 @@ WellKnownSidType.AuthenticatedUserSid, null);
try try
{ {
rv = Common.Retry(nameof(SystemClipboard.ContainsText), () => { return SystemClipboard.ContainsText(); }, (log) => Log(log)); rv = IpcChannelHelper.Retry(nameof(SystemClipboard.ContainsText), () => { return SystemClipboard.ContainsText(); }, (log) => Log(log));
} }
catch (ExternalException e) catch (ExternalException e)
{ {
@@ -471,7 +471,7 @@ WellKnownSidType.AuthenticatedUserSid, null);
try try
{ {
rv = Common.Retry(nameof(SystemClipboard.GetFileDropList), () => { return SystemClipboard.GetFileDropList(); }, (log) => Log(log)); rv = IpcChannelHelper.Retry(nameof(SystemClipboard.GetFileDropList), () => { return SystemClipboard.GetFileDropList(); }, (log) => Log(log));
} }
catch (ExternalException e) catch (ExternalException e)
{ {
@@ -493,7 +493,7 @@ WellKnownSidType.AuthenticatedUserSid, null);
try try
{ {
rv = Common.Retry(nameof(SystemClipboard.GetImage), () => { return SystemClipboard.GetImage(); }, (log) => Log(log)); rv = IpcChannelHelper.Retry(nameof(SystemClipboard.GetImage), () => { return SystemClipboard.GetImage(); }, (log) => Log(log));
} }
catch (ExternalException e) catch (ExternalException e)
{ {
@@ -515,7 +515,7 @@ WellKnownSidType.AuthenticatedUserSid, null);
try try
{ {
rv = Common.Retry(nameof(SystemClipboard.GetText), () => { return SystemClipboard.GetText(format); }, (log) => Log(log)); rv = IpcChannelHelper.Retry(nameof(SystemClipboard.GetText), () => { return SystemClipboard.GetText(format); }, (log) => Log(log));
} }
catch (ExternalException e) catch (ExternalException e)
{ {
@@ -539,7 +539,7 @@ WellKnownSidType.AuthenticatedUserSid, null);
{ {
try try
{ {
_ = Common.Retry( _ = IpcChannelHelper.Retry(
nameof(SystemClipboard.SetImage), nameof(SystemClipboard.SetImage),
() => () =>
{ {
@@ -568,7 +568,7 @@ WellKnownSidType.AuthenticatedUserSid, null);
{ {
try try
{ {
_ = Common.Retry( _ = IpcChannelHelper.Retry(
nameof(SystemClipboard.SetText), nameof(SystemClipboard.SetText),
() => () =>
{ {
@@ -600,44 +600,4 @@ WellKnownSidType.AuthenticatedUserSid, null);
{ {
internal const int QUIT_CMD = 0x409; internal const int QUIT_CMD = 0x409;
} }
internal sealed partial class Common
{
internal static bool IpcChannelCreated { get; set; }
internal static T Retry<T>(string name, Func<T> func, Action<string> log, Action preRetry = null)
{
int count = 0;
do
{
try
{
T rv = func();
if (count > 0)
{
log($"Trace: {name} has been successful after {count} retry.");
}
return rv;
}
catch (Exception)
{
count++;
preRetry?.Invoke();
if (count > 10)
{
throw;
}
Application.DoEvents();
Thread.Sleep(200);
}
}
while (true);
}
}
} }

View File

@@ -1036,7 +1036,7 @@ internal static class Clipboard
{ {
try try
{ {
_ = Common.Retry( _ = IpcChannelHelper.Retry(
nameof(SystemClipboard.SetFileDropList), nameof(SystemClipboard.SetFileDropList),
() => () =>
{ {
@@ -1073,7 +1073,7 @@ internal static class Clipboard
{ {
try try
{ {
_ = Common.Retry( _ = IpcChannelHelper.Retry(
nameof(SystemClipboard.SetImage), nameof(SystemClipboard.SetImage),
() => () =>
{ {
@@ -1104,7 +1104,7 @@ internal static class Clipboard
{ {
try try
{ {
_ = Common.Retry( _ = IpcChannelHelper.Retry(
nameof(SystemClipboard.SetText), nameof(SystemClipboard.SetText),
() => () =>
{ {

File diff suppressed because it is too large Load Diff

View File

@@ -295,9 +295,9 @@ internal static class Helper
return; return;
} }
if (!Common.IpcChannelCreated) if (!IpcChannelHelper.IpcChannelCreated)
{ {
Logger.TelemetryLogTrace($"{nameof(Common.IpcChannelCreated)} = {Common.IpcChannelCreated}. {Logger.GetStackTrace(new StackTrace())}", SeverityLevel.Warning); Logger.TelemetryLogTrace($"{nameof(IpcChannelHelper.IpcChannelCreated)} = {IpcChannelHelper.IpcChannelCreated}. {Logger.GetStackTrace(new StackTrace())}", SeverityLevel.Warning);
return; return;
} }

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Windows.Forms;
#if !MM_HELPER
using Thread = MouseWithoutBorders.Core.Thread;
#endif
namespace MouseWithoutBorders.Core;
internal static class IpcChannelHelper
{
internal static bool IpcChannelCreated { get; set; }
internal static T Retry<T>(string name, Func<T> func, Action<string> log, Action preRetry = null)
{
int count = 0;
do
{
try
{
T rv = func();
if (count > 0)
{
log($"Trace: {name} has been successful after {count} retry.");
}
return rv;
}
catch (Exception)
{
count++;
preRetry?.Invoke();
if (count > 10)
{
throw;
}
Application.DoEvents();
Thread.Sleep(200);
}
}
while (true);
}
}

View File

@@ -198,7 +198,6 @@ internal static class Logger
} }
Logger.DumpProgramLogs(sb, level); Logger.DumpProgramLogs(sb, level);
Logger.DumpOtherLogs(sb, level);
Logger.DumpStaticTypes(sb, level); Logger.DumpStaticTypes(sb, level);
log = string.Format( log = string.Format(
@@ -240,19 +239,16 @@ internal static class Logger
_ = Logger.PrivateDump(sb, AllLogs, "[Program logs]\r\n===============\r\n", 0, level, false); _ = Logger.PrivateDump(sb, AllLogs, "[Program logs]\r\n===============\r\n", 0, level, false);
} }
internal static void DumpOtherLogs(StringBuilder sb, int level)
{
_ = Logger.PrivateDump(sb, new Common(), "[Other Logs]\r\n===============\r\n", 0, level, false);
}
internal static void DumpStaticTypes(StringBuilder sb, int level) internal static void DumpStaticTypes(StringBuilder sb, int level)
{ {
var staticTypes = new List<Type> var staticTypes = new List<Type>
{ {
typeof(Clipboard), typeof(Clipboard),
typeof(Common),
typeof(DragDrop), typeof(DragDrop),
typeof(Encryption), typeof(Encryption),
typeof(Event), typeof(Event),
typeof(IpcChannelHelper),
typeof(InitAndCleanup), typeof(InitAndCleanup),
typeof(Helper), typeof(Helper),
typeof(Launch), typeof(Launch),

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using MouseWithoutBorders.Core;
namespace MouseWithoutBorders namespace MouseWithoutBorders
{ {
public partial class SetupPage2b : SettingsFormPage public partial class SetupPage2b : SettingsFormPage

View File

@@ -15,6 +15,8 @@ using System.Globalization;
using System.Reflection; using System.Reflection;
using System.Windows.Forms; using System.Windows.Forms;
using MouseWithoutBorders.Core;
namespace MouseWithoutBorders namespace MouseWithoutBorders
{ {
internal partial class FrmAbout : System.Windows.Forms.Form, IDisposable internal partial class FrmAbout : System.Windows.Forms.Form, IDisposable

View File

@@ -6,6 +6,8 @@ using System;
using System.Globalization; using System.Globalization;
using System.Windows.Forms; using System.Windows.Forms;
using MouseWithoutBorders.Core;
namespace MouseWithoutBorders namespace MouseWithoutBorders
{ {
public partial class FrmMessage : System.Windows.Forms.Form public partial class FrmMessage : System.Windows.Forms.Form

View File

@@ -6,6 +6,7 @@ using System;
using System.Windows.Forms; using System.Windows.Forms;
using MouseWithoutBorders.Class; using MouseWithoutBorders.Class;
using MouseWithoutBorders.Core;
namespace MouseWithoutBorders namespace MouseWithoutBorders
{ {

View File

@@ -49,6 +49,9 @@
<Compile Include="..\Class\IClipboardHelper.cs"> <Compile Include="..\Class\IClipboardHelper.cs">
<Link>IClipboardHelper.cs</Link> <Link>IClipboardHelper.cs</Link>
</Compile> </Compile>
<Compile Include="..\Core\IpcChannelHelper.cs">
<Link>IpcChannelHelper.cs</Link>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,9 +1,38 @@
[Program logs] [Program logs]
=============== ===============
= System.String[] = System.String[]
[Other Logs] [Clipboard]
===============
Comma = System.Char[]
--System.Char[] = System.Char[]: N/A
Star = System.Char[]
--System.Char[] = System.Char[]: N/A
NullSeparator = System.Char[]
--System.Char[] = System.Char[]: N/A
lastClipboardEventTime = 0
clipboardCopiedTime = 0
<LastIDWithClipboardData>k__BackingField = NONE
<NextClipboardViewer>k__BackingField = 0
<IsClipboardDataImage>k__BackingField = False
lastClipboardObject =
<HasSwitchedMachineSinceLastCopy>k__BackingField = False
ClipboardThreadOldLock = Lock
--_owningThreadId = 0
--_state = 0
--_recursionCount = 0
--_spinCount = 22
--_waiterStartTimeMs = 0
--s_contentionCount = 0
--s_maxSpinCount = 22
--s_minSpinCountForAdaptiveSpin = -100
BIG_CLIPBOARD_DATA_TIMEOUT = 30000
MAX_CLIPBOARD_DATA_SIZE_CAN_BE_SENT_INSTANTLY_TCP = 1048576
MAX_CLIPBOARD_FILE_SIZE_CAN_BE_SENT = 104857600
TEXT_HEADER_SIZE = 12
DATA_SIZE = 48
TEXT_TYPE_SEP = {4CFF57F7-BEDD-43d5-AE8F-27A61E886F2F}
[Common]
=============== ===============
= MouseWithoutBorders.Common
screenWidth = 0 screenWidth = 0
screenHeight = 0 screenHeight = 0
lastX = 0 lastX = 0
@@ -46,7 +75,6 @@ avgSendTime = 0
maxSendTime = 0 maxSendTime = 0
totalSendCount = 0 totalSendCount = 0
totalSendTime = 0 totalSendTime = 0
<IpcChannelCreated>k__BackingField = False
TOGGLE_ICONS_SIZE = 4 TOGGLE_ICONS_SIZE = 4
ICON_ONE = 0 ICON_ONE = 0
ICON_ALL = 1 ICON_ALL = 1
@@ -55,36 +83,6 @@ ICON_BIG_CLIPBOARD = 3
ICON_ERROR = 4 ICON_ERROR = 4
JUST_GOT_BACK_FROM_SCREEN_SAVER = 9999 JUST_GOT_BACK_FROM_SCREEN_SAVER = 9999
NETWORK_STREAM_BUF_SIZE = 1048576 NETWORK_STREAM_BUF_SIZE = 1048576
[Clipboard]
===============
Comma = System.Char[]
--System.Char[] = System.Char[]: N/A
Star = System.Char[]
--System.Char[] = System.Char[]: N/A
NullSeparator = System.Char[]
--System.Char[] = System.Char[]: N/A
lastClipboardEventTime = 0
clipboardCopiedTime = 0
<LastIDWithClipboardData>k__BackingField = NONE
<NextClipboardViewer>k__BackingField = 0
<IsClipboardDataImage>k__BackingField = False
lastClipboardObject =
<HasSwitchedMachineSinceLastCopy>k__BackingField = False
ClipboardThreadOldLock = Lock
--_owningThreadId = 0
--_state = 0
--_recursionCount = 0
--_spinCount = 22
--_waiterStartTimeMs = 0
--s_contentionCount = 0
--s_maxSpinCount = 22
--s_minSpinCountForAdaptiveSpin = -100
BIG_CLIPBOARD_DATA_TIMEOUT = 30000
MAX_CLIPBOARD_DATA_SIZE_CAN_BE_SENT_INSTANTLY_TCP = 1048576
MAX_CLIPBOARD_FILE_SIZE_CAN_BE_SENT = 104857600
TEXT_HEADER_SIZE = 12
DATA_SIZE = 48
TEXT_TYPE_SEP = {4CFF57F7-BEDD-43d5-AE8F-27A61E886F2F}
[DragDrop] [DragDrop]
=============== ===============
isDragging = False isDragging = False
@@ -174,6 +172,9 @@ actualLastPos = {X=0,Y=0}
--Empty = {X=0,Y=0} --Empty = {X=0,Y=0}
myLastX = 0 myLastX = 0
myLastY = 0 myLastY = 0
[IpcChannelHelper]
===============
<IpcChannelCreated>k__BackingField = False
[InitAndCleanup] [InitAndCleanup]
=============== ===============
initDone = False initDone = False
@@ -440,6 +441,7 @@ WM_LBUTTONDBLCLK = 515
WM_RBUTTONDBLCLK = 518 WM_RBUTTONDBLCLK = 518
WM_MBUTTONDBLCLK = 521 WM_MBUTTONDBLCLK = 521
WM_MOUSEWHEEL = 522 WM_MOUSEWHEEL = 522
WM_MOUSEHWHEEL = 526
WM_KEYDOWN = 256 WM_KEYDOWN = 256
WM_KEYUP = 257 WM_KEYUP = 257
WM_SYSKEYDOWN = 260 WM_SYSKEYDOWN = 260

View File

@@ -117,7 +117,6 @@ public static class LoggerTests
// copied from DumpObjects in Logger.cs // copied from DumpObjects in Logger.cs
var sb = new StringBuilder(1000000); var sb = new StringBuilder(1000000);
Logger.DumpProgramLogs(sb, settingsDumpObjectsLevel); Logger.DumpProgramLogs(sb, settingsDumpObjectsLevel);
Logger.DumpOtherLogs(sb, settingsDumpObjectsLevel);
Logger.DumpStaticTypes(sb, settingsDumpObjectsLevel); Logger.DumpStaticTypes(sb, settingsDumpObjectsLevel);
var actual = sb.ToString(); var actual = sb.ToString();

View File

@@ -106,7 +106,7 @@
<controls:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left"> <controls:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Margin="-12,0,0,0" Orientation="Vertical"> <StackPanel Margin="-12,0,0,0" Orientation="Vertical">
<HyperlinkButton x:Uid="Settings_GeneralPage_About_GithubLink_Hyperlink" NavigateUri="https://go.microsoft.com/fwlink/?linkid=2310837" /> <HyperlinkButton x:Uid="Settings_GeneralPage_About_GithubLink_Hyperlink" NavigateUri="https://go.microsoft.com/fwlink/?linkid=2310837" />
<HyperlinkButton x:Uid="Settings_GeneralPage_About_SDKDocs_Hyperlink" NavigateUri="https://go.microsoft.com/fwlink/?linkid=2310639" /> <HyperlinkButton x:Uid="Settings_GeneralPage_About_SDKDocs_Hyperlink" NavigateUri="https://aka.ms/cmdpalextensions-devdocs" />
</StackPanel> </StackPanel>
</controls:SettingsCard> </controls:SettingsCard>
</controls:SettingsExpander.Items> </controls:SettingsExpander.Items>

View File

@@ -4,6 +4,7 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToys.Interop; using PowerToys.Interop;
@@ -21,15 +22,20 @@ internal sealed partial class CropAndLockReparentCommand : InvokableCommand
public override CommandResult Invoke() public override CommandResult Invoke()
{ {
try Task.Run(async () =>
{ {
using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockReparentEvent()); await Task.Delay(500);
evt.Set(); try
return CommandResult.Dismiss(); {
} using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockReparentEvent());
catch (Exception ex) evt.Set();
{ }
return CommandResult.ShowToast($"Failed to start Crop and Lock (Reparent): {ex.Message}"); catch
} {
// Ignore errors after dismissing
}
});
return CommandResult.Dismiss();
} }
} }

View File

@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToys.Interop;
namespace PowerToysExtension.Commands;
/// <summary>
/// Triggers Crop and Lock screenshot mode via the shared event.
/// </summary>
internal sealed partial class CropAndLockScreenshotCommand : InvokableCommand
{
public CropAndLockScreenshotCommand()
{
Name = "Crop and Lock (Screenshot)";
}
public override CommandResult Invoke()
{
Task.Run(async () =>
{
await Task.Delay(500);
try
{
using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockScreenshotEvent());
evt.Set();
}
catch
{
// Ignore errors after dismissing
}
});
return CommandResult.Dismiss();
}
}

View File

@@ -4,6 +4,7 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToys.Interop; using PowerToys.Interop;
@@ -21,15 +22,20 @@ internal sealed partial class CropAndLockThumbnailCommand : InvokableCommand
public override CommandResult Invoke() public override CommandResult Invoke()
{ {
try Task.Run(async () =>
{ {
using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockThumbnailEvent()); await Task.Delay(500);
evt.Set(); try
return CommandResult.Dismiss(); {
} using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockThumbnailEvent());
catch (Exception ex) evt.Set();
{ }
return CommandResult.ShowToast($"Failed to start Crop and Lock (Thumbnail): {ex.Message}"); catch
} {
// Ignore errors after dismissing
}
});
return CommandResult.Dismiss();
} }
} }

View File

@@ -34,6 +34,13 @@ internal sealed class CropAndLockModuleCommandProvider : ModuleCommandProvider
Subtitle = Resources.CropAndLock_Thumbnail_Subtitle, Subtitle = Resources.CropAndLock_Thumbnail_Subtitle,
Icon = icon, Icon = icon,
}; };
yield return new ListItem(new CropAndLockScreenshotCommand())
{
Title = Resources.CropAndLock_Screenshot_Title,
Subtitle = Resources.CropAndLock_Screenshot_Subtitle,
Icon = icon,
};
} }
yield return new ListItem(new OpenInSettingsCommand(module, title)) yield return new ListItem(new OpenInSettingsCommand(module, title))

View File

@@ -375,6 +375,24 @@ namespace PowerToysExtension.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Crop and Lock (Screenshot).
/// </summary>
internal static string CropAndLock_Screenshot_Title {
get {
return ResourceManager.GetString("CropAndLock_Screenshot_Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create a cropped screenshot window.
/// </summary>
internal static string CropAndLock_Screenshot_Subtitle {
get {
return ResourceManager.GetString("CropAndLock_Screenshot_Subtitle", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Launch Environment Variables editor. /// Looks up a localized string similar to Launch Environment Variables editor.
/// </summary> /// </summary>

View File

@@ -260,6 +260,12 @@
<data name="CropAndLock_Thumbnail_Subtitle" xml:space="preserve"> <data name="CropAndLock_Thumbnail_Subtitle" xml:space="preserve">
<value>Create a cropped thumbnail window</value> <value>Create a cropped thumbnail window</value>
</data> </data>
<data name="CropAndLock_Screenshot_Title" xml:space="preserve">
<value>Crop and Lock (Screenshot)</value>
</data>
<data name="CropAndLock_Screenshot_Subtitle" xml:space="preserve">
<value>Create a cropped screenshot window</value>
</data>
<data name="CropAndLock_Settings_Subtitle" xml:space="preserve"> <data name="CropAndLock_Settings_Subtitle" xml:space="preserve">
<value>Open Crop and Lock settings</value> <value>Open Crop and Lock settings</value>
</data> </data>

View File

@@ -7,7 +7,7 @@ Palette, and use the "Create a new extension" command. That will set up a
project for you, with the packaging, dependencies, and basic program structure project for you, with the packaging, dependencies, and basic program structure
ready to go. ready to go.
To view the full docs, you can head over to [our docs site](https://go.microsoft.com/fwlink/?linkid=2310639) To view the full docs, you can head over to [our docs site](https://aka.ms/cmdpalextensions-devdocs)
There are samples of just about everything you can do in [the samples project]. There are samples of just about everything you can do in [the samples project].
Head over there to see basic usage of the APIs. Head over there to see basic usage of the APIs.

View File

@@ -148,16 +148,19 @@ std::optional<std::wstring> dispatch_json_action_to_module(const json::JsonObjec
return result; return result;
} }
void send_json_config_to_module(const std::wstring& module_key, const std::wstring& settings) void send_json_config_to_module(const std::wstring& module_key, const std::wstring& settings, bool hotkeyUpdated)
{ {
auto moduleIt = modules().find(module_key); auto moduleIt = modules().find(module_key);
if (moduleIt != modules().end()) if (moduleIt != modules().end())
{ {
moduleIt->second->set_config(settings.c_str()); moduleIt->second->set_config(settings.c_str());
moduleIt->second.remove_hotkey_records(); if (hotkeyUpdated)
moduleIt->second.update_hotkeys(); {
moduleIt->second.UpdateHotkeyEx(); moduleIt->second.remove_hotkey_records();
moduleIt->second.update_hotkeys();
moduleIt->second.UpdateHotkeyEx();
}
} }
} }
@@ -166,7 +169,22 @@ void dispatch_json_config_to_modules(const json::JsonObject& powertoys_configs)
for (const auto& powertoy_element : powertoys_configs) for (const auto& powertoy_element : powertoys_configs)
{ {
const auto element = powertoy_element.Value().Stringify(); const auto element = powertoy_element.Value().Stringify();
send_json_config_to_module(powertoy_element.Key().c_str(), element.c_str());
/* As PowerToys Run hotkeys are not registered by the runner, hotkey updates are
* triggered only when hotkey properties change to avoid incorrect conflict detection;
* otherwise, the existing logic remains.
*/
auto settings = powertoy_element.Value().GetObjectW();
bool hotkeyUpdated = true;
if (settings.HasKey(L"properties"))
{
const auto properties = settings.GetNamedObject(L"properties");
// Currently, only PowerToys Run settings use the 'hotkey_changed' property.
json::get(properties, L"hotkey_changed", hotkeyUpdated, true);
}
send_json_config_to_module(powertoy_element.Key().c_str(), element.c_str(), hotkeyUpdated);
} }
}; };

View File

@@ -11,11 +11,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{ {
public static readonly HotkeySettings DefaultReparentHotkeyValue = new HotkeySettings(true, true, false, true, 0x52); // Ctrl+Win+Shift+R public static readonly HotkeySettings DefaultReparentHotkeyValue = new HotkeySettings(true, true, false, true, 0x52); // Ctrl+Win+Shift+R
public static readonly HotkeySettings DefaultThumbnailHotkeyValue = new HotkeySettings(true, true, false, true, 0x54); // Ctrl+Win+Shift+T public static readonly HotkeySettings DefaultThumbnailHotkeyValue = new HotkeySettings(true, true, false, true, 0x54); // Ctrl+Win+Shift+T
public static readonly HotkeySettings DefaultScreenshotHotkeyValue = new HotkeySettings(true, true, false, true, 0x53); // Ctrl+Win+Shift+S
public CropAndLockProperties() public CropAndLockProperties()
{ {
ReparentHotkey = new KeyboardKeysProperty(DefaultReparentHotkeyValue); ReparentHotkey = new KeyboardKeysProperty(DefaultReparentHotkeyValue);
ThumbnailHotkey = new KeyboardKeysProperty(DefaultThumbnailHotkeyValue); ThumbnailHotkey = new KeyboardKeysProperty(DefaultThumbnailHotkeyValue);
ScreenshotHotkey = new KeyboardKeysProperty(DefaultScreenshotHotkeyValue);
} }
[JsonPropertyName("reparent-hotkey")] [JsonPropertyName("reparent-hotkey")]
@@ -23,5 +25,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("thumbnail-hotkey")] [JsonPropertyName("thumbnail-hotkey")]
public KeyboardKeysProperty ThumbnailHotkey { get; set; } public KeyboardKeysProperty ThumbnailHotkey { get; set; }
[JsonPropertyName("screenshot-hotkey")]
public KeyboardKeysProperty ScreenshotHotkey { get; set; }
} }
} }

View File

@@ -44,6 +44,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
() => Properties.ThumbnailHotkey.Value, () => Properties.ThumbnailHotkey.Value,
value => Properties.ThumbnailHotkey.Value = value ?? CropAndLockProperties.DefaultThumbnailHotkeyValue, value => Properties.ThumbnailHotkey.Value = value ?? CropAndLockProperties.DefaultThumbnailHotkeyValue,
"CropAndLock_ThumbnailActivation_Shortcut"), "CropAndLock_ThumbnailActivation_Shortcut"),
new HotkeyAccessor(
() => Properties.ScreenshotHotkey.Value,
value => Properties.ScreenshotHotkey.Value = value ?? CropAndLockProperties.DefaultScreenshotHotkeyValue,
"CropAndLock_ScreenshotActivation_Shortcut"),
}; };
return hotkeyAccessors.ToArray(); return hotkeyAccessors.ToArray();

View File

@@ -17,6 +17,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public const string DefaultLatitude = "0.0"; public const string DefaultLatitude = "0.0";
public const string DefaultLongitude = "0.0"; public const string DefaultLongitude = "0.0";
public const string DefaultScheduleMode = "Off"; public const string DefaultScheduleMode = "Off";
public const bool DefaultWallpaperEnabled = false;
public const bool DefaultWallpaperVirtualDesktopEnabled = false;
public const int DefaultWallpaperStyle = 0;
public const string DefaultWallpaperPath = "";
public static readonly HotkeySettings DefaultToggleThemeHotkey = new HotkeySettings(true, true, false, true, 0x44); // Ctrl+Win+Shift+D public static readonly HotkeySettings DefaultToggleThemeHotkey = new HotkeySettings(true, true, false, true, 0x44); // Ctrl+Win+Shift+D
public LightSwitchProperties() public LightSwitchProperties()
@@ -30,6 +34,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
SunriseOffset = new IntProperty(DefaultSunriseOffset); SunriseOffset = new IntProperty(DefaultSunriseOffset);
SunsetOffset = new IntProperty(DefaultSunsetOffset); SunsetOffset = new IntProperty(DefaultSunsetOffset);
ScheduleMode = new StringProperty(DefaultScheduleMode); ScheduleMode = new StringProperty(DefaultScheduleMode);
WallpaperEnabled = new BoolProperty(DefaultWallpaperEnabled);
WallpaperVirtualDesktopEnabled = new BoolProperty(DefaultWallpaperVirtualDesktopEnabled);
WallpaperStyleLight = new IntProperty(DefaultWallpaperStyle);
WallpaperStyleDark = new IntProperty(DefaultWallpaperStyle);
WallpaperPathLight = new StringProperty(DefaultWallpaperPath);
WallpaperPathDark = new StringProperty(DefaultWallpaperPath);
ToggleThemeHotkey = new KeyboardKeysProperty(DefaultToggleThemeHotkey); ToggleThemeHotkey = new KeyboardKeysProperty(DefaultToggleThemeHotkey);
} }
@@ -62,5 +72,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("toggle-theme-hotkey")] [JsonPropertyName("toggle-theme-hotkey")]
public KeyboardKeysProperty ToggleThemeHotkey { get; set; } public KeyboardKeysProperty ToggleThemeHotkey { get; set; }
[JsonPropertyName("wallpaperEnabled")]
public BoolProperty WallpaperEnabled { get; set; }
[JsonPropertyName("wallpaperVirtualDesktopEnabled")]
public BoolProperty WallpaperVirtualDesktopEnabled { get; set; }
[JsonPropertyName("wallpaperStyleLight")]
public IntProperty WallpaperStyleLight { get; set; }
[JsonPropertyName("wallpaperStyleDark")]
public IntProperty WallpaperStyleDark { get; set; }
[JsonPropertyName("wallpaperPathLight")]
public StringProperty WallpaperPathLight { get; set; }
[JsonPropertyName("wallpaperPathDark")]
public StringProperty WallpaperPathDark { get; set; }
} }
} }

View File

@@ -60,6 +60,12 @@ namespace Settings.UI.Library
Latitude = new StringProperty(Properties.Latitude.Value), Latitude = new StringProperty(Properties.Latitude.Value),
Longitude = new StringProperty(Properties.Longitude.Value), Longitude = new StringProperty(Properties.Longitude.Value),
ToggleThemeHotkey = new KeyboardKeysProperty(Properties.ToggleThemeHotkey.Value), ToggleThemeHotkey = new KeyboardKeysProperty(Properties.ToggleThemeHotkey.Value),
WallpaperEnabled = new BoolProperty(Properties.WallpaperEnabled.Value),
WallpaperVirtualDesktopEnabled = new BoolProperty(Properties.WallpaperVirtualDesktopEnabled.Value),
WallpaperStyleLight = new IntProperty((int)Properties.WallpaperStyleLight.Value),
WallpaperStyleDark = new IntProperty((int)Properties.WallpaperStyleDark.Value),
WallpaperPathLight = new StringProperty(Properties.WallpaperPathLight.Value),
WallpaperPathDark = new StringProperty(Properties.WallpaperPathDark.Value),
}, },
}; };
} }

View File

@@ -94,6 +94,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("generate_thumbnails_from_files")] [JsonPropertyName("generate_thumbnails_from_files")]
public bool GenerateThumbnailsFromFiles { get; set; } public bool GenerateThumbnailsFromFiles { get; set; }
[JsonPropertyName("hotkey_changed")]
public bool HotkeyChanged { get; set; } = false;
[CmdConfigureIgnoreAttribute] [CmdConfigureIgnoreAttribute]
public HotkeySettings DefaultOpenPowerLauncher => new HotkeySettings(false, false, true, false, 32); public HotkeySettings DefaultOpenPowerLauncher => new HotkeySettings(false, false, true, false, 32);

View File

@@ -16,6 +16,8 @@
<controls:ShortcutWithTextLabelControl x:Name="ReparentHotkeyControl" x:Uid="Oobe_CropAndLock_HowToUse_Reparent" /> <controls:ShortcutWithTextLabelControl x:Name="ReparentHotkeyControl" x:Uid="Oobe_CropAndLock_HowToUse_Reparent" />
<controls:ShortcutWithTextLabelControl x:Name="ScreenshotHotkeyControl" x:Uid="Oobe_CropAndLock_HowToUse_Screenshot" />
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" /> <Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />

View File

@@ -35,8 +35,10 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
protected override void OnNavigatedTo(NavigationEventArgs e) protected override void OnNavigatedTo(NavigationEventArgs e)
{ {
ViewModel.LogOpeningModuleEvent(); ViewModel.LogOpeningModuleEvent();
ReparentHotkeyControl.Keys = SettingsRepository<CropAndLockSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ReparentHotkey.Value.GetKeysList(); ReparentHotkeyControl.Keys = SettingsRepository<CropAndLockSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ReparentHotkey.Value.GetKeysList();
ThumbnailHotkeyControl.Keys = SettingsRepository<CropAndLockSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ThumbnailHotkey.Value.GetKeysList(); ThumbnailHotkeyControl.Keys = SettingsRepository<CropAndLockSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ThumbnailHotkey.Value.GetKeysList();
ScreenshotHotkeyControl.Keys = SettingsRepository<CropAndLockSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ScreenshotHotkey.Value.GetKeysList();
} }
protected override void OnNavigatedFrom(NavigationEventArgs e) protected override void OnNavigatedFrom(NavigationEventArgs e)

View File

@@ -44,6 +44,12 @@
AllowDisable="True" AllowDisable="True"
HotkeySettings="{x:Bind Path=ViewModel.ReparentActivationShortcut, Mode=TwoWay}" /> HotkeySettings="{x:Bind Path=ViewModel.ReparentActivationShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="CropAndLock_ScreenshotActivation_Shortcut" HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
<controls:ShortcutControl
MinWidth="{StaticResource SettingActionControlMinWidth}"
AllowDisable="True"
HotkeySettings="{x:Bind Path=ViewModel.ScreenshotActivationShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup> </controls:SettingsGroup>
</StackPanel> </StackPanel>
</controls:SettingsPageControl.ModuleContent> </controls:SettingsPageControl.ModuleContent>

View File

@@ -9,6 +9,7 @@
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers" xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
xmlns:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewModels="using:Microsoft.PowerToys.Settings.UI.ViewModels" xmlns:viewModels="using:Microsoft.PowerToys.Settings.UI.ViewModels"
d:DataContext="{d:DesignInstance Type=viewModels:LightSwitchViewModel}" d:DataContext="{d:DesignInstance Type=viewModels:LightSwitchViewModel}"
@@ -17,6 +18,12 @@
<local:NavigablePage.Resources> <local:NavigablePage.Resources>
<converters:TimeSpanToFriendlyTimeConverter x:Key="TimeSpanToFriendlyTimeConverter" /> <converters:TimeSpanToFriendlyTimeConverter x:Key="TimeSpanToFriendlyTimeConverter" />
<converters:StringToDoubleConverter x:Key="StringToDoubleConverter" /> <converters:StringToDoubleConverter x:Key="StringToDoubleConverter" />
<tkconverters:BoolNegationConverter x:Key="BoolNegationConverter" />
<tkconverters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<tkconverters:BoolToVisibilityConverter
x:Key="BoolToInvertedVisibilityConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
</local:NavigablePage.Resources> </local:NavigablePage.Resources>
<Grid> <Grid>
<controls:SettingsPageControl <controls:SettingsPageControl
@@ -211,6 +218,124 @@
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items> </tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander> </tkcontrols:SettingsExpander>
<tkcontrols:SettingsExpander
x:Uid="LightSwitch_WallpaperExpander"
HeaderIcon="{ui:FontIcon Glyph=&#xE7f9;}"
IsExpanded="True">
<ToggleSwitch AutomationProperties.AutomationId="Toggle_WallpaperSwitch" IsOn="{x:Bind ViewModel.IsWallpaperEnabled, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Left">
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="20px" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="LightSwitch_WallpaperImageUnavailable"
Grid.Row="0"
Grid.Column="0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Visibility="{x:Bind ViewModel.IsLightWallpaperValid, Converter={StaticResource BoolToInvertedVisibilityConverter}, Mode=OneWay}" />
<Image
Grid.Row="0"
Grid.Column="0"
VerticalAlignment="Center"
Source="{x:Bind ViewModel.WallpaperSourceLight, Mode=OneWay}"
Tag="Light" />
<TextBlock
x:Uid="LightSwitch_WallpaperImageUnavailable"
Grid.Row="0"
Grid.Column="2"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Visibility="{x:Bind ViewModel.IsDarkWallpaperValid, Converter={StaticResource BoolToInvertedVisibilityConverter}, Mode=OneWay}" />
<Image
Grid.Row="0"
Grid.Column="2"
Source="{x:Bind ViewModel.WallpaperSourceDark, Mode=OneWay}"
Tag="Dark" />
</Grid>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="LightSwitch_WallpaperSelect">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock
x:Uid="LightSwitch_WallpaperLight"
Grid.Row="2"
Grid.Column="0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<Button
x:Uid="LightSwitch_WallpaperBrowse"
Grid.Row="2"
Grid.Column="0"
AutomationProperties.AutomationId="Pick_ButtonLight"
Click="PickWallpaper_Click"
Tag="Light" />
<TextBlock
x:Uid="LightSwitch_WallpaperDark"
Grid.Row="2"
Grid.Column="2"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<Button
x:Uid="LightSwitch_WallpaperBrowse"
Grid.Row="2"
Grid.Column="2"
AutomationProperties.AutomationId="Pick_ButtonDark"
Click="PickWallpaper_Click"
Tag="Dark" />
</StackPanel>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="LightSwitch_WallpaperStyle">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock
x:Uid="LightSwitch_WallpaperLight"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<ComboBox AutomationProperties.AutomationId="Toggle_WallpaperStyleSwitchLight" SelectedIndex="{x:Bind ViewModel.WallpaperStyleLight, Mode=TwoWay}">
<ComboBoxItem x:Uid="LightSwitch_StyleCenter" AutomationProperties.AutomationId="CenterCBItem_StyleSwitch" />
<ComboBoxItem x:Uid="LightSwitch_StyleTile" AutomationProperties.AutomationId="TileCBItem_StyleSwitch" />
<ComboBoxItem x:Uid="LightSwitch_StyleStretch" AutomationProperties.AutomationId="StretchCBItem_StyleSwitch" />
<ComboBoxItem x:Uid="LightSwitch_StyleFit" AutomationProperties.AutomationId="FitCBItem_StyleSwitch" />
<ComboBoxItem x:Uid="LightSwitch_StyleFill" AutomationProperties.AutomationId="FillCBItem_StyleSwitch" />
<ComboBoxItem x:Uid="LightSwitch_StyleSpan" AutomationProperties.AutomationId="SpanCBItem_StyleSwitch" />
</ComboBox>
<TextBlock
x:Uid="LightSwitch_WallpaperDark"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<ComboBox AutomationProperties.AutomationId="Toggle_WallpaperStyleSwitchDark" SelectedIndex="{x:Bind ViewModel.WallpaperStyleDark, Mode=TwoWay}">
<ComboBoxItem x:Uid="LightSwitch_StyleCenter" AutomationProperties.AutomationId="CenterCBItem_StyleSwitch" />
<ComboBoxItem x:Uid="LightSwitch_StyleTile" AutomationProperties.AutomationId="TileCBItem_StyleSwitch" />
<ComboBoxItem x:Uid="LightSwitch_StyleStretch" AutomationProperties.AutomationId="StretchCBItem_StyleSwitch" />
<ComboBoxItem x:Uid="LightSwitch_StyleFit" AutomationProperties.AutomationId="FitCBItem_StyleSwitch" />
<ComboBoxItem x:Uid="LightSwitch_StyleFill" AutomationProperties.AutomationId="FillCBItem_StyleSwitch" />
<ComboBoxItem x:Uid="LightSwitch_StyleSpan" AutomationProperties.AutomationId="SpanCBItem_StyleSwitch" />
</ComboBox>
</StackPanel>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="LightSwitch_WallpaperVirtualDesktop" Visibility="{x:Bind ViewModel.Is24H2OrLater, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneTime}">
<ToggleSwitch AutomationProperties.AutomationId="Toggle_WallpaperVirtualDesktopSwitch" IsOn="{x:Bind ViewModel.IsVirtualDesktopEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}" Visibility="{x:Bind ViewModel.Is24H2OrLater, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneTime}">
<tkcontrols:SettingsCard.Header>
<TextBlock x:Uid="LightSwitch_VirtualDesktopInfo" TextWrapping="Wrap" />
</tkcontrols:SettingsCard.Header>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}" Visibility="{x:Bind ViewModel.Is24H2OrLater, Converter={StaticResource BoolToInvertedVisibilityConverter}, Mode=OneTime}">
<tkcontrols:SettingsCard.Header>
<TextBlock x:Uid="LightSwitch_DetectWindows24H2Info" TextWrapping="Wrap" />
</tkcontrols:SettingsCard.Header>
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup> </controls:SettingsGroup>
<!-- Force mode buttons --> <!-- Force mode buttons -->
<!--<tkcontrols:SettingsCard <!--<tkcontrols:SettingsCard

View File

@@ -16,9 +16,13 @@ using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using Microsoft.Windows.Storage.Pickers;
using PowerToys.GPOWrapper; using PowerToys.GPOWrapper;
using Settings.UI.Library; using Settings.UI.Library;
using Windows.Devices.Geolocation; using Windows.Devices.Geolocation;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Microsoft.PowerToys.Settings.UI.Views namespace Microsoft.PowerToys.Settings.UI.Views
{ {
@@ -185,7 +189,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
// need to save the values // need to save the values
this.ViewModel.Latitude = latitude.ToString(CultureInfo.InvariantCulture); this.ViewModel.Latitude = latitude.ToString(CultureInfo.InvariantCulture);
this.ViewModel.Longitude = longitude.ToString(CultureInfo.InvariantCulture); this.ViewModel.Longitude = longitude.ToString(CultureInfo.InvariantCulture);
this.ViewModel.SyncButtonInformation = $"{this.ViewModel.Latitude}<7D>, {this.ViewModel.Longitude}<7D>"; this.ViewModel.SyncButtonInformation = $"{this.ViewModel.Latitude}<7D>, {this.ViewModel.Longitude}<7D>";
var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day); var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
@@ -391,5 +395,50 @@ namespace Microsoft.PowerToys.Settings.UI.Views
this.LocationWarningBar.Visibility = Visibility.Visible; this.LocationWarningBar.Visibility = Visibility.Visible;
} }
} }
private async void PickWallpaper_Click(object sender, RoutedEventArgs e)
{
var tag = (sender as Button).Tag as string;
var fileOpenPicker = new FileOpenPicker((sender as Button).XamlRoot.ContentIslandEnvironment.AppWindowId);
string[] extensions = { ".jpg", ".jpeg", ".bmp", ".dib", ".png", ".jfif", ".jpe", ".gif", ".tif", ".tiff", ".wdp", ".heic", ".heif", ".heics", ".heifs", ".hif", ".avci", ".avcs", ".avif", ".avifs", ".jxr", ".jxl", ".webp" };
foreach (var ext in extensions)
{
fileOpenPicker.FileTypeFilter.Add(ext);
}
fileOpenPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
var selectedFile = await fileOpenPicker.PickSingleFileAsync();
if (selectedFile == null)
{
return;
}
if (!string.IsNullOrEmpty(ViewModel.WallpaperPathLight) && tag == "Light")
{
LightSwitchViewModel.DeleteFile(ViewModel.WallpaperPathLight);
ViewModel.WallpaperPathLight = string.Empty;
}
else if (!string.IsNullOrEmpty(ViewModel.WallpaperPathDark) && tag == "Dark")
{
LightSwitchViewModel.DeleteFile(ViewModel.WallpaperPathDark);
ViewModel.WallpaperPathDark = string.Empty;
}
var srcFile = await StorageFile.GetFileFromPathAsync(selectedFile.Path);
var settingsFolder = await StorageFolder.GetFolderFromPathAsync(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Microsoft\\PowerToys\\LightSwitch");
var dstFile = await settingsFolder.CreateFileAsync($"{tag}{DateTime.Now.ToFileTime()}{srcFile.FileType}", CreationCollisionOption.ReplaceExisting);
await FileIO.WriteBufferAsync(dstFile, await FileIO.ReadBufferAsync(srcFile));
if (tag == "Light")
{
ViewModel.WallpaperPathLight = dstFile.Path;
}
else if (tag == "Dark")
{
ViewModel.WallpaperPathDark = dstFile.Path;
}
}
} }
} }

Some files were not shown because too many files have changed in this diff Show More