Compare commits

...

54 Commits

Author SHA1 Message Date
Shawn Yuan
37ecf5712a add telemetry log code.
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-07-17 14:34:33 +08:00
Shuai Yuan
ade6b30a6f remove dev spec
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-16 16:36:54 +08:00
Shuai Yuan
75088a9fed Update dev spec
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-16 15:37:39 +08:00
Shuai Yuan
f94ca33a07 Fix ShortcutConflictViewModel issue.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-15 17:15:43 +08:00
Shuai Yuan
09eab7dd3a update ui and localization.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-15 11:26:26 +08:00
Shuai Yuan
2d00566975 Added support for changing shortcut in conflict window.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-14 21:54:47 +08:00
Shuai Yuan
5c93a3ed5f Update doc
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-11 11:29:11 +08:00
Shuai Yuan
13d6ec8058 Update UI for hotkey conflict dtection dialog.
Added navigation to hotkey conflict detection dialog card.

Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-10 15:53:32 +08:00
Shuai Yuan
fb28e3d184 Added a draft dialog to show all hotkey conflict info.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 17:43:20 +08:00
Shuai Yuan
0694c98972 Added conflict tooltip for all modules.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 15:45:31 +08:00
Shuai Yuan
988000744e Rebase and resolve compiling issues.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 14:00:47 +08:00
Shuai Yuan
9a975cb193 Added living shortcut conflict detection in shortcut dialog.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:35 +08:00
Shuai Yuan
cc9b060a24 update ui after change the hotkeys.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:35 +08:00
Shuai Yuan
890a85bff6 add conflict info support in shortcut dialog
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:35 +08:00
Shuai Yuan
a5f140df17 Fixed enable/disable module issue for conflict
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:35 +08:00
Shuai Yuan
63257e928d Fixed mouse utils crash issue.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:35 +08:00
Shuai Yuan
875593e32e Added hotkey conflict detection for mouse without border.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:35 +08:00
Shuai Yuan
7078d05b77 Remove all conflict info form general page.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:34 +08:00
Shuai Yuan
36d568bfde Added shortcut conflict ui for all modules.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:34 +08:00
Shuai Yuan
5d37b29418 Added hotkey conflict ui for colorpicker.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:34 +08:00
Shuai Yuan
83d3b51fbd Implemented shortcut ui for advancedPaste
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:34 +08:00
Shuai Yuan
9b7cc006bb added UI change.
Update Shortcut control.
Update UI for hotkey conflict in AdvancedPaste module.

Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:34 +08:00
Shuai Yuan
6995ebd8fd Added IPC for hotkey conflict button control.
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:34 +08:00
Shuai Yuan
768c062068 add hotkey conflict IPC for module settings pages
Signed-off-by: Shuai Yuan <shuai.yuan.zju@gmail.com>
2025-07-08 13:03:34 +08:00
Shawn Yuan
567e431544 restore deps
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-07-08 13:03:34 +08:00
Shuai Yuan
ff9e328102 Code rebase.
remove the ui for shortcut conflicts
and prepair for rebase to the new ui design.

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-07-08 13:03:34 +08:00
Niels Laute
2dc6bdb000 Update App.xaml 2025-07-04 12:23:54 +02:00
Niels Laute
48b782a74a Merge branch 'main' into niels9001/new-dashboard-ux 2025-07-04 12:23:20 +02:00
leileizhang
0c425fd1d7 [Fix][CmdPal] CmdPal Apps extension is missing all Win32 applications (#40392)
<!-- 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
### Root Cause
Calling `"CreateFile"` without an explicit `EntryPoint` fails because
kernel32.dll only exports `CreateFileA` and `CreateFileW`.

### Fix
Add EntryPoint = "CreateFileW"

### How to reproduce
The issue is reproducible when the PC is set to Chinese or another
non-English language.

![image](https://github.com/user-attachments/assets/2bdfd644-3ccf-4ad0-a470-2bd7de29049c)

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

- [x] **Closes:** #40389, #40340 #40388 #40378 #40341
- [ ] **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
2025-07-04 16:18:18 +08:00
Kai Tao
03a9ac1ac7 [UI automation] workspaces ui automation (#39812)
<!-- 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
- [ ] **Communication:** I've discussed this with core contributors
already. If 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


![image](https://github.com/user-attachments/assets/1be219be-1d06-432c-8acb-e3a2ba56d1b6)


https://microsoft.visualstudio.com/Dart/_build/results?buildId=125637396&view=results

---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
Co-authored-by: Jerry Xu <n.xu@outlook.com>
Co-authored-by: Zhaopeng Wang <zhaopengwang@microsoft.com>
Co-authored-by: Xiaofeng Wang (from Dev Box) <xiaofengwang@microsoft.com>
Co-authored-by: Mengyuan <162882040+chenmy77@users.noreply.github.com>
Co-authored-by: yaqingmi <miyaqing01@gmail.com>
Co-authored-by: Clint Rutkas <clint@rutkas.com>
Co-authored-by: Yaqing Mi (from Dev Box) <yaqingmi@microsoft.com>
Co-authored-by: XiaofengWang <709586527@qq.com>
Co-authored-by: zhaopeng wang <33367956+wang563681252@users.noreply.github.com>
Co-authored-by: Laszlo Nemeth <57342539+donlaci@users.noreply.github.com>
Co-authored-by: RokyZevon <12629919+RokyZevon@users.noreply.github.com>
Co-authored-by: Yu Leng <42196638+moooyo@users.noreply.github.com>
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Davide Giacometti <25966642+davidegiacometti@users.noreply.github.com>
Co-authored-by: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com>
Co-authored-by: ruslanlap <106077551+ruslanlap@users.noreply.github.com>
Co-authored-by: Muhammad Danish <mdanishkhdev@gmail.com>
Co-authored-by: Bennett Blodinger <benwa@users.noreply.github.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
Co-authored-by: Ionuț Manța <ionut@janeasystems.com>
Co-authored-by: Hao Liu <liuhaobupt@163.com>
Co-authored-by: OlegHarchevkin <40352094+OlegKharchevkin@users.noreply.github.com>
Co-authored-by: dcog989 <89043002+dcog989@users.noreply.github.com>
Co-authored-by: PesBandi <127593627+PesBandi@users.noreply.github.com>
Co-authored-by: Stefan Markovic <57057282+stefansjfw@users.noreply.github.com>
Co-authored-by: Typpi <20943337+Nick2bad4u@users.noreply.github.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
Co-authored-by: Carlos Zamora <carlos.zamora@microsoft.com>
Co-authored-by: Abhyudit <64366765+bitmap4@users.noreply.github.com>
Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>
Co-authored-by: Ved Nig <vednig12@outlook.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Aung Khaing Khant <aungkhaingkhant.dev@gmail.com>
Co-authored-by: Aung Khaing Khant <aungkhaingkhant@advent-soft.com>
Co-authored-by: Dustin L. Howett <duhowett@microsoft.com>
Co-authored-by: leileizhang <leilzh@microsoft.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
Co-authored-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: cryolithic <cryolithic@gmail.com>
Co-authored-by: Lemonyte <49930425+lemonyte@users.noreply.github.com>
Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
Co-authored-by: Corey Hayward <72159232+CoreyHayward@users.noreply.github.com>
Co-authored-by: Jerry Xu <nxu@microsoft.com>
Co-authored-by: Shawn Yuan <shuaiyuan@microsoft.com>
Co-authored-by: Kayla Cinnamon <cinnamon@microsoft.com>
Co-authored-by: Jeremy Sinclair <4016293+snickler@users.noreply.github.com>
2025-07-04 10:07:37 +08:00
Copilot
837d5ca543 Fix signing configuration: Remove 25 obsolete file references from ESRPSigning_core.json (#40241)
## Summary

This PR fixes the signing pipeline by removing 25 obsolete file
references from `ESRPSigning_core.json` that were causing "0 files out
of: 0 files" errors during the signing process. These references pointed
to files that are either no longer built or were never produced by the
current project structure.

## Root Cause Analysis

The signing configuration contained references to files that fall into
three categories:

1. **Static libraries incorrectly listed as DLLs** - Projects configured
as `StaticLibrary` don't produce `.dll` files
2. **Obsolete/non-existent projects** - References to projects that were
removed or renamed
3. **WinExe projects incorrectly listed as producing DLLs** - C#
projects with `OutputType=WinExe` only produce `.exe` files, not `.dll`
files

## Changes Made

### Static Libraries (3 files removed):
- `Notifications.dll` - notifications project is a StaticLibrary
- `os-detection.dll` - no corresponding project found
- `Telemetry.dll` - telemetry projects are StaticLibraries

### Obsolete Projects (3 files removed):
- `fancyzones.dll` - FancyZones now produces `PowerToys.FancyZones.exe`
- `Wox.dll` - only `Wox.Plugin.dll` and `Wox.Infrastructure.dll` exist
- Duplicate `PowerToys.ManagedTelemetry.dll` entry

### WinExe Projects (19 files removed):
**Preview/Thumbnail Handlers (11 files):**
All preview and thumbnail handler C# projects have `OutputType=WinExe`
and only produce `.exe` files:
- Removed `.dll` entries for: GcodePreviewHandler,
MarkdownPreviewHandler, MonacoPreviewHandler, PdfPreviewHandler,
QoiPreviewHandler, SvgPreviewHandler, GcodeThumbnailProvider,
PdfThumbnailProvider, QoiThumbnailProvider, StlThumbnailProvider,
SvgThumbnailProvider

**Application Modules (8 files):**
- `PowerToys.WorkspacesEditor.dll` and
`PowerToys.WorkspacesLauncherUI.dll`
- `PowerToys.Awake.dll` and `PowerToys.ImageResizer.dll` 
- `PowerToys.ColorPickerUI.dll` and `PowerToys.PowerOCR.dll`
- `PowerToys.PowerAccent.dll` and `PowerToys.PowerLauncher.dll`

## Verification

All removed entries were verified by:
1. Checking project files for `OutputType` and `ConfigurationType`
settings
2. Confirming `AssemblyName` and `TargetName` properties
3. Ensuring no actual built artifacts are affected

The signing process should now successfully find all expected files and
eliminate the "0 files out of: 0 files" pattern.

Fixes #40240.

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Share your feedback on Copilot coding agent for the chance to win a
$200 gift card! Click
[here](https://survey.alchemer.com/s3/8343779/Copilot-Coding-agent) to
start the survey.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: vanzue <69313318+vanzue@users.noreply.github.com>
2025-07-04 09:50:22 +08:00
Kai Tao
2ff5adbdd4 [Settings] Complete the settings deeplink (#40376)
<!-- 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
Many settings page's deep link is not implemented, this PR complete them
and make them aligned with the settings page.

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

- [ ] **Closes:** #xxx
- [ ] **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
2025-07-04 09:41:19 +08:00
Niels Laute
faebd21a2d [Settings] String updates and minor UX tweaks (#40249)
## Summary of the Pull Request

- I asked Copilot nicely to rewrite some of our strings to be inline
with the Windows writing style guide and to be less verbose or provide a
better explaination.
- Since the Sound expander on the Always on Top page had a single
setting, turning that into a card.

Before:

![image](https://github.com/user-attachments/assets/04ec18f1-06a4-4185-ac50-655070dcf3fc)

After:

![image](https://github.com/user-attachments/assets/e5d62e00-5d0a-4a2d-9011-493232689f41)


Simplified UI + less XAML for diagnostics data:

Before:

![image](https://github.com/user-attachments/assets/c0b0468b-7587-4684-8fbf-bad856e4d30c)

After:

![image](https://github.com/user-attachments/assets/185f2ff8-a3b1-49bd-8617-24b2c5a454a2)



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

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

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

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

---------

Co-authored-by: Kai Tao <kaitao@microsoft.com>
2025-07-03 16:20:22 +02:00
Niels Laute
6412d5722e [UX] Settings - fix header position (#40160)
## Summary of the Pull Request
Due to a padding in the scrollviewer hosting the settingspage content,
the title drifts away upon resizing the settings window.
This PR fixes that issue by adding the (20px) padding to the maxwidth of
the header textblock.

Before:

![header](https://github.com/user-attachments/assets/6704a59d-843c-49a7-a109-e22e2cecccfe)

After:

![header2](https://github.com/user-attachments/assets/c9b0aa46-fcab-43ac-be87-ea423d810606)



## PR Checklist

- [x] **Closes:** #40159
- [ ] **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
2025-07-03 04:33:56 -05:00
Niels Laute
6c933f3c4e Update SettingsPageControl.xaml 2025-07-03 11:20:55 +02:00
Niels Laute
2dcf2020f5 Styles refactor 2025-07-01 11:58:48 +02:00
Niels Laute
5f748f8206 Bringing back the icon for the shortcut control 2025-07-01 11:40:07 +02:00
Niels Laute
d1e702cf9b Fix 2025-07-01 11:36:53 +02:00
Niels Laute
dbd2ea5231 Moving Update and ShortcutConflicts to dedicated controls so they can be reused 2025-07-01 11:24:37 +02:00
Niels Laute
3e14b3a8c2 Add missing tag 2025-07-01 10:57:02 +02:00
Niels Laute
cb3afc743f Merge branch 'main' into niels9001/new-dashboard-ux 2025-06-27 17:46:14 +02:00
Niels Laute
ab584188d4 More changes 2025-06-27 17:33:13 +02:00
Niels Laute
40fc4561ee More shortcut visual refactoring 2025-06-27 16:20:50 +02:00
Niels Laute
c1564db865 CI fixes 2025-06-27 10:51:09 +02:00
Niels Laute
5457b27d54 Adding icons to the quick actions 2025-06-25 16:54:36 +02:00
Niels Laute
e08ef9c3fe Enabling resizing 2025-06-24 16:48:26 +02:00
Niels Laute
f162449850 Updating shortcut strings 2025-06-24 16:24:02 +02:00
Niels Laute
ef51252d70 Refactor KeyVisual for better UX customization 2025-06-24 13:00:16 +02:00
Niels Laute
1d438426b3 More shortcuts 2025-06-23 16:25:14 +02:00
Niels Laute
0aff6426c0 Adding shortcuts view 2025-06-23 13:11:02 +02:00
Niels Laute
d28ebcd3ce Remove KBM dashboard item and related code
Eliminated the DashboardModuleKBMItem, its template, and associated logic from the dashboard. This simplifies the DashboardViewModel and DashboardPage.xaml by removing the display and dynamic updating of Keyboard Manager remappings.
2025-06-23 11:05:26 +02:00
Niels Laute
faab625a25 Dashboard changes 2025-06-20 18:28:28 +02:00
Niels Laute
e1bcf62b84 Adding Card control 2025-06-20 16:02:29 +02:00
Niels Laute
72f62c5718 Cleaning up Controls folder 2025-06-20 16:02:19 +02:00
184 changed files with 9890 additions and 1557 deletions

View File

@@ -141,6 +141,7 @@ boxmodel
BPBF
bpmf
bpp
breadcrumb
Breadcrumb
Browsable
BROWSEINFO

View File

@@ -11,14 +11,13 @@
"PowerToys.ActionRunner.exe",
"PowerToys.Update.exe",
"PowerToys.BackgroundActivatorDLL.dll",
"Notifications.dll",
"os-detection.dll",
"PowerToys.exe",
"PowerToys.FilePreviewCommon.dll",
"PowerToys.Interop.dll",
"Tools\\PowerToys.BugReportTool.exe",
"StylesReportTool\\PowerToys.StylesReportTool.exe",
"Telemetry.dll",
"CalculatorEngineCommon.dll",
"PowerToys.ManagedTelemetry.dll",
"PowerToys.ManagedCommon.dll",
@@ -33,7 +32,7 @@
"PowerToys.AlwaysOnTopModuleInterface.dll",
"PowerToys.CmdNotFoundModuleInterface.dll",
"PowerToys.CmdNotFound.dll",
"PowerToys.ColorPicker.dll",
"PowerToys.ColorPickerUI.dll",
@@ -54,7 +53,7 @@
"PowerToys.Awake.exe",
"PowerToys.Awake.dll",
"fancyzones.dll",
"PowerToys.FancyZonesEditor.exe",
"PowerToys.FancyZonesEditor.dll",
"PowerToys.FancyZonesEditorCommon.dll",
@@ -136,7 +135,7 @@
"PowerToys.PowerLauncher.dll",
"PowerToys.PowerLauncher.exe",
"PowerToys.PowerLauncher.Telemetry.dll",
"Wox.dll",
"Wox.Infrastructure.dll",
"Wox.Plugin.dll",
"RunPlugins\\Calculator\\Microsoft.PowerToys.Run.Plugin.Calculator.dll",
@@ -275,16 +274,16 @@
"Mono.Cecil.Pdb.dll",
"Mono.Cecil.Rocks.dll",
"Newtonsoft.Json.dll",
"Newtonsoft.Json.Bson.dll",
"NLog.dll",
"HtmlAgilityPack.dll",
"Markdig.Signed.dll",
"HelixToolkit.dll",
"HelixToolkit.Core.Wpf.dll",
"Mages.Core.dll",
"JetBrains.Annotations.dll",
"NLog.Extensions.Logging.dll",
"getfilesiginforedist.dll",
"concrt140_app.dll",
"msvcp140_1_app.dll",
"msvcp140_2_app.dll",
@@ -294,22 +293,8 @@
"vcomp140_app.dll",
"vcruntime140_1_app.dll",
"vcruntime140_app.dll",
"WinUI3Apps\\CommunityToolkit.Labs.WinUI.SettingsControls.dll",
"UnicodeInformation.dll",
"Vanara.Core.dll",
"Vanara.PInvoke.ComCtl32.dll",
"Vanara.PInvoke.Cryptography.dll",
"Vanara.PInvoke.Gdi32.dll",
"Vanara.PInvoke.Kernel32.dll",
"Vanara.PInvoke.Ole.dll",
"Vanara.PInvoke.Rpc.dll",
"Vanara.PInvoke.Security.dll",
"Vanara.PInvoke.Shared.dll",
"Vanara.PInvoke.Shell32.dll",
"Vanara.PInvoke.ShlwApi.dll",
"Vanara.PInvoke.User32.dll",
"WinUI3Apps\\clrcompression.dll",
"WinUI3Apps\\Microsoft.Graphics.Canvas.Interop.dll",
"Microsoft.Web.WebView2.Core.dll",
"Microsoft.Web.WebView2.WinForms.dll",
"Microsoft.Web.WebView2.Wpf.dll",
@@ -336,7 +321,7 @@
"Testably.Abstractions.FileSystem.Interface.dll",
"WinUI3Apps\\Testably.Abstractions.FileSystem.Interface.dll",
"ColorCode.Core.dll",
"ColorCode.UWP.dll",
"UnitsNet.dll",
"UtfUnknown.dll",
"Wpf.Ui.dll"

View File

@@ -11,26 +11,35 @@ namespace Common.UI
{
public enum SettingsWindow
{
Overview = 0,
Dashboard = 0,
Overview,
AlwaysOnTop,
Awake,
ColorPicker,
CmdNotFound,
FancyZones,
FileLocksmith,
Run,
ImageResizer,
KBM,
MouseUtils,
MouseWithoutBorders,
Peek,
PowerAccent,
PowerLauncher,
PowerPreview,
PowerRename,
FileExplorer,
ShortcutGuide,
Hosts,
MeasureTool,
PowerOCR,
Workspaces,
RegistryPreview,
CropAndLock,
EnvironmentVariables,
Dashboard,
AdvancedPaste,
Workspaces,
NewPlus,
CmdPal,
ZoomIt,
}
@@ -39,14 +48,22 @@ namespace Common.UI
{
switch (value)
{
case SettingsWindow.Dashboard:
return "Dashboard";
case SettingsWindow.Overview:
return "Overview";
case SettingsWindow.AlwaysOnTop:
return "AlwaysOnTop";
case SettingsWindow.Awake:
return "Awake";
case SettingsWindow.ColorPicker:
return "ColorPicker";
case SettingsWindow.CmdNotFound:
return "CmdNotFound";
case SettingsWindow.FancyZones:
return "FancyZones";
case SettingsWindow.FileLocksmith:
return "FileLocksmith";
case SettingsWindow.Run:
return "Run";
case SettingsWindow.ImageResizer:
@@ -55,6 +72,16 @@ namespace Common.UI
return "KBM";
case SettingsWindow.MouseUtils:
return "MouseUtils";
case SettingsWindow.MouseWithoutBorders:
return "MouseWithoutBorders";
case SettingsWindow.Peek:
return "Peek";
case SettingsWindow.PowerAccent:
return "PowerAccent";
case SettingsWindow.PowerLauncher:
return "PowerLauncher";
case SettingsWindow.PowerPreview:
return "PowerPreview";
case SettingsWindow.PowerRename:
return "PowerRename";
case SettingsWindow.FileExplorer:
@@ -67,18 +94,18 @@ namespace Common.UI
return "MeasureTool";
case SettingsWindow.PowerOCR:
return "PowerOcr";
case SettingsWindow.Workspaces:
return "Workspaces";
case SettingsWindow.RegistryPreview:
return "RegistryPreview";
case SettingsWindow.CropAndLock:
return "CropAndLock";
case SettingsWindow.EnvironmentVariables:
return "EnvironmentVariables";
case SettingsWindow.Dashboard:
return "Dashboard";
case SettingsWindow.AdvancedPaste:
return "AdvancedPaste";
case SettingsWindow.Workspaces:
return "Workspaces";
case SettingsWindow.NewPlus:
return "NewPlus";
case SettingsWindow.CmdPal:
return "CmdPal";
case SettingsWindow.ZoomIt:

View File

@@ -21,7 +21,7 @@ public static partial class Kernel32
out int pBytesReturned,
IntPtr lpOverlapped);
[LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16, EntryPoint = "CreateFileW")]
public static partial int CreateFile(
string lpFileName,
FileAccessType dwDesiredAccess,

View File

@@ -4,18 +4,9 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using Windows.Devices.Display.Core;
using Windows.Foundation.Metadata;
using static Microsoft.PowerToys.UITest.UITestBase.NativeMethods;
using static Microsoft.PowerToys.UITest.WindowHelper;
namespace Microsoft.PowerToys.UITest
{

View File

@@ -11,7 +11,7 @@ using System.Threading.Tasks;
namespace Microsoft.PowerToys.UITest
{
internal static class WindowHelper
public static class WindowHelper
{
internal const string AdministratorPrefix = "Administrator: ";

View File

@@ -50,6 +50,7 @@ namespace
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_NAME[] = L"hotkeyName";
const wchar_t JSON_KEY_PASTE_AS_PLAIN_HOTKEY[] = L"paste-as-plain-hotkey";
const wchar_t JSON_KEY_ADVANCED_PASTE_UI_HOTKEY[] = L"advanced-paste-ui-hotkey";
const wchar_t JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY[] = L"paste-as-markdown-hotkey";
@@ -57,9 +58,29 @@ namespace
const wchar_t JSON_KEY_IS_ADVANCED_AI_ENABLED[] = L"IsAdvancedAIEnabled";
const wchar_t JSON_KEY_SHOW_CUSTOM_PREVIEW[] = L"ShowCustomPreview";
const wchar_t JSON_KEY_VALUE[] = L"value";
const wchar_t JSON_KEY_IMAGE_TO_TEXT_HOTKEY[] = L"image-to-text";
const wchar_t JSON_KEY_PASTE_AS_TXT_FILE_HOTKEY[] = L"paste-as-txt-file";
const wchar_t JSON_KEY_PASTE_AS_PNG_FILE_HOTKEY[] = L"paste-as-png-file";
const wchar_t JSON_KEY_PASTE_AS_HTML_FILE_HOTKEY[] = L"paste-as-html-file";
const wchar_t JSON_KEY_TRANSCODE_TO_MP3_HOTKEY[] = L"transcode-to-mp3";
const wchar_t JSON_KEY_TRANSCODE_TO_MP4_HOTKEY[] = L"transcode-to-mp4";
const wchar_t OPENAI_VAULT_RESOURCE[] = L"https://platform.openai.com/api-keys";
const wchar_t OPENAI_VAULT_USERNAME[] = L"PowerToys_AdvancedPaste_OpenAIKey";
const wchar_t PASTE_AS_PLAIN_HOTKEY_NAME[] = L"PasteAsPlainTextShortcut";
const wchar_t ADVANCED_PASTE_UI_HOTKEY_NAME[] = L"AdvancedPasteUIShortcut";
const wchar_t PASTE_AS_MARKDOWN_HOTKEY_NAME[] = L"PasteAsMarkdownShortcut";
const wchar_t PASTE_AS_JSON_HOTKEY_NAME[] = L"PasteAsJsonShortcut";
// additional actions hotkeys
const wchar_t IMAGE_TO_TEXT_HOTKEY_NAME[] = L"ImageToTextShortcut";
const wchar_t PASTE_AS_TXT_FILE_HOTKEY_NAME[] = L"PasteAsTxtFileShortcut";
const wchar_t PASTE_AS_PNG_FILE_HOTKEY_NAME[] = L"PasteAsPngFileShortcut";
const wchar_t PASTE_AS_HTML_FILE_HOTKEY_NAME[] = L"PasteAsHtmlFileShortcut";
const wchar_t TRANSCODE_TO_MP3_HOTKEY_NAME[] = L"TranscodeToMp3Shortcut";
const wchar_t TRANSCODE_TO_MP4_HOTKEY_NAME[] = L"TranscodeToMp4Shortcut";
}
class AdvancedPaste : public PowertoyModuleIface
@@ -76,10 +97,10 @@ private:
static const constexpr int NUM_DEFAULT_HOTKEYS = 4;
Hotkey m_paste_as_plain_hotkey = { .win = true, .ctrl = true, .shift = false, .alt = true, .key = 'V' };
Hotkey m_advanced_paste_ui_hotkey = { .win = true, .ctrl = false, .shift = true, .alt = false, .key = 'V' };
Hotkey m_paste_as_markdown_hotkey{};
Hotkey m_paste_as_json_hotkey{};
Hotkey m_paste_as_plain_hotkey = { .win = true, .ctrl = true, .shift = false, .alt = true, .key = 'V', .name = PASTE_AS_PLAIN_HOTKEY_NAME };
Hotkey m_advanced_paste_ui_hotkey = { .win = true, .ctrl = false, .shift = true, .alt = false, .key = 'V', .name = ADVANCED_PASTE_UI_HOTKEY_NAME };
Hotkey m_paste_as_markdown_hotkey{ .name = PASTE_AS_MARKDOWN_HOTKEY_NAME };
Hotkey m_paste_as_json_hotkey{ .name = PASTE_AS_JSON_HOTKEY_NAME };
template<class Id>
struct ActionData
@@ -93,6 +114,7 @@ private:
using CustomAction = ActionData<int>;
std::vector<CustomAction> m_custom_actions;
std::vector<std::wstring> m_custom_action_hotkey_names;
bool m_is_advanced_ai_enabled = false;
bool m_preview_custom_format_output = true;
@@ -102,7 +124,9 @@ private:
try
{
const auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(keyName);
return parse_single_hotkey(jsonHotkeyObject);
Hotkey hotkey = parse_single_hotkey(jsonHotkeyObject);
hotkey.name = get_hotkey_name(keyName);
return hotkey;
}
catch (...)
{
@@ -112,6 +136,38 @@ private:
return {};
}
const wchar_t* get_hotkey_name(const wchar_t* name)
{
if (wcscmp(name, JSON_KEY_PASTE_AS_PLAIN_HOTKEY) == 0)
return PASTE_AS_PLAIN_HOTKEY_NAME;
if (wcscmp(name, JSON_KEY_ADVANCED_PASTE_UI_HOTKEY) == 0)
return ADVANCED_PASTE_UI_HOTKEY_NAME;
if (wcscmp(name, JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY) == 0)
return PASTE_AS_MARKDOWN_HOTKEY_NAME;
if (wcscmp(name, JSON_KEY_PASTE_AS_JSON_HOTKEY) == 0)
return PASTE_AS_JSON_HOTKEY_NAME;
return nullptr;
}
const wchar_t* get_additional_action_hotkey_name(std::wstring name)
{
if (name == JSON_KEY_IMAGE_TO_TEXT_HOTKEY)
return IMAGE_TO_TEXT_HOTKEY_NAME;
if (name == JSON_KEY_PASTE_AS_TXT_FILE_HOTKEY)
return PASTE_AS_TXT_FILE_HOTKEY_NAME;
if (name == JSON_KEY_PASTE_AS_PNG_FILE_HOTKEY)
return PASTE_AS_PNG_FILE_HOTKEY_NAME;
if (name == JSON_KEY_PASTE_AS_HTML_FILE_HOTKEY)
return PASTE_AS_HTML_FILE_HOTKEY_NAME;
if (name == JSON_KEY_TRANSCODE_TO_MP3_HOTKEY)
return TRANSCODE_TO_MP3_HOTKEY_NAME;
if (name == JSON_KEY_TRANSCODE_TO_MP4_HOTKEY)
return TRANSCODE_TO_MP4_HOTKEY_NAME;
return nullptr;
}
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
{
try
@@ -122,6 +178,7 @@ private:
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
hotkey.name = jsonHotkeyObject.GetNamedString(JSON_KEY_NAME).c_str();
return hotkey;
}
catch (...)
@@ -140,6 +197,7 @@ private:
jsonObject.SetNamedValue(JSON_KEY_SHIFT, json::value(hotkey.shift));
jsonObject.SetNamedValue(JSON_KEY_CTRL, json::value(hotkey.ctrl));
jsonObject.SetNamedValue(JSON_KEY_CODE, json::value(hotkey.key));
jsonObject.SetNamedValue(JSON_KEY_NAME, json::value(hotkey.name));
return jsonObject;
}
@@ -247,12 +305,13 @@ private:
if (action.HasKey(JSON_KEY_SHORTCUT))
{
const AdditionalAction additionalAction
AdditionalAction additionalAction
{
actionName.c_str(),
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT))
};
additionalAction.hotkey.name = get_additional_action_hotkey_name(additionalAction.id);
m_additional_actions.push_back(additionalAction);
}
else
@@ -334,12 +393,14 @@ private:
if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
{
const CustomAction customActionData
CustomAction customActionData
{
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
};
m_custom_action_hotkey_names.push_back(L"CustomAction_" + std::to_wstring(customActionData.id));
customActionData.hotkey.name = m_custom_action_hotkey_names.back().c_str();
m_custom_actions.push_back(customActionData);
}
}

View File

@@ -30,6 +30,9 @@ namespace
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_VALUE[] = L"value";
const wchar_t JSON_KEY_NAME[] = L"hotkeyName";
const wchar_t REPARENT_SHORTCUT_NAME[] = L"ReparentHotkey";
const wchar_t THUMBNAIL_SHORTCUT_NAME[] = L"ThumbnailHotkey";
}
BOOL APIENTRY DllMain( HMODULE /*hModule*/,
@@ -262,6 +265,7 @@ private:
_temp_reparent.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
_temp_reparent.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
_temp_reparent.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
_temp_reparent.name = REPARENT_SHORTCUT_NAME;
m_reparent_hotkey = _temp_reparent;
}
catch (...)
@@ -277,6 +281,7 @@ private:
_temp_thumbnail.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
_temp_thumbnail.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
_temp_thumbnail.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
_temp_thumbnail.name = THUMBNAIL_SHORTCUT_NAME;
m_thumbnail_hotkey = _temp_thumbnail;
}
catch (...)
@@ -319,8 +324,8 @@ private:
HANDLE m_hProcess = nullptr;
// 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_thumbnail_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'T' };
Hotkey m_reparent_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'R', .name = REPARENT_SHORTCUT_NAME };
Hotkey m_thumbnail_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'T', .name = THUMBNAIL_SHORTCUT_NAME };
HANDLE m_reparent_event_handle;
HANDLE m_thumbnail_event_handle;

View File

@@ -40,6 +40,8 @@ namespace
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
const wchar_t JSON_KEY_NAME[] = L"hotkeyName";
const wchar_t ACTIVATION_SHORTCUT_NAME[] = L"ActivationShortcut";
}
class MeasureTool : public PowertoyModuleIface
@@ -67,6 +69,7 @@ private:
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
catch (...)
{
@@ -86,6 +89,7 @@ private:
m_hotkey.alt = false;
m_hotkey.shift = true;
m_hotkey.key = 'M';
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
}

View File

@@ -27,6 +27,9 @@ namespace
const wchar_t JSON_KEY_SHAKING_INTERVAL_MS[] = L"shaking_interval_ms";
const wchar_t JSON_KEY_SHAKING_FACTOR[] = L"shaking_factor";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
const wchar_t JSON_KEY_NAME[] = L"hotkeyName";
const wchar_t ACTIVATION_SHORTCUT_NAME[] = L"ActivationShortcut";
}
extern "C" IMAGE_DOS_HEADER __ImageBase;
@@ -484,6 +487,10 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
m_hotkey.modifiersMask = MOD_SHIFT | MOD_WIN;
m_hotkey.vkCode = 0x46; // F key
}
if (m_hotkey.name == nullptr)
{
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
}
else
{

View File

@@ -19,6 +19,7 @@ namespace
const wchar_t JSON_KEY_HIGHLIGHT_FADE_DURATION_MS[] = L"highlight_fade_duration_ms";
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t JSON_KEY_SPOTLIGHT_MODE[] = L"spotlight_mode";
const wchar_t ACTIVATION_HOTKEY_NAME[] = L"ActivationShortcut";
}
extern "C" IMAGE_DOS_HEADER __ImageBase;
@@ -389,6 +390,10 @@ public:
m_hotkey.modifiersMask = MOD_SHIFT | MOD_WIN;
m_hotkey.vkCode = 0x48; // H key
}
if (m_hotkey.name == nullptr)
{
m_hotkey.name = ACTIVATION_HOTKEY_NAME;
}
m_highlightSettings = highlightSettings;
}
};

View File

@@ -44,6 +44,8 @@ namespace
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
const wchar_t JSON_KEY_NAME[] = L"hotkeyName";
const wchar_t ACTIVATION_SHORTCUT_NAME[] = L"ActivationShortcut";
}
// Implement the PowerToy Module Interface and all the required methods.
@@ -81,6 +83,7 @@ private:
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
catch (...)
{
@@ -100,6 +103,7 @@ private:
m_hotkey.shift = true;
m_hotkey.ctrl = false;
m_hotkey.key = 'D';
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
}

View File

@@ -21,6 +21,8 @@ namespace
const wchar_t JSON_KEY_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED[] = L"crosshairs_is_fixed_length_enabled";
const wchar_t JSON_KEY_CROSSHAIRS_FIXED_LENGTH[] = L"crosshairs_fixed_length";
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t ACTIVATION_SHORTCUT_NAME[] = L"ActivationShortcut";
}
extern "C" IMAGE_DOS_HEADER __ImageBase;
@@ -394,6 +396,10 @@ public:
m_hotkey.modifiersMask = MOD_WIN | MOD_ALT;
m_hotkey.vkCode = 0x50; // P key
}
if (m_hotkey.name == nullptr)
{
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
m_inclusiveCrosshairsSettings = inclusiveCrosshairsSettings;
}

View File

@@ -17,6 +17,14 @@
HINSTANCE g_hInst_MouseWithoutBorders = 0;
namespace
{
const wchar_t SWITCH2ALLPC_SHORTCUT_NAME[] = L"HotKeySwitch2AllPC";
const wchar_t LOCKMACHINE_SHORTCUT_NAME[] = L"HotKeyLockMachine";
const wchar_t RECONNECT_SHORTCUT_NAME[] = L"HotKeyReconnect";
const wchar_t TOGGLEEASYMOUSE_SHORTCUT_NAME[] = L"HotKeyToggleEasyMouse";
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
@@ -126,6 +134,32 @@ private:
bool run_in_service_mode = false;
PROCESS_INFORMATION p_info = {};
// Helper function to convert HotkeyObject to Hotkey struct
Hotkey ConvertHotkeyObjectToHotkey(const PowerToysSettings::HotkeyObject& hotkeyObj, const wchar_t* name)
{
Hotkey hotkey;
hotkey.win = hotkeyObj.win_pressed();
hotkey.ctrl = hotkeyObj.ctrl_pressed();
hotkey.alt = hotkeyObj.alt_pressed();
hotkey.shift = hotkeyObj.shift_pressed();
hotkey.key = static_cast<unsigned char>(hotkeyObj.get_code());
hotkey.name = name;
return hotkey;
}
// Helper function to create a default disabled hotkey
Hotkey CreateDisabledHotkey(const wchar_t* name)
{
Hotkey hotkey;
hotkey.win = false;
hotkey.ctrl = false;
hotkey.alt = false;
hotkey.shift = false;
hotkey.key = 0;
hotkey.name = name;
return hotkey;
}
bool is_enabled_by_default() const override
{
return false;
@@ -556,6 +590,77 @@ public:
return m_enabled;
}
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
constexpr size_t num_hotkeys = 4; // We have 4 hotkeys
if (hotkeys && buffer_size >= num_hotkeys)
{
try
{
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME);
// Cache the raw JSON object to avoid multiple parsing
json::JsonObject root_json = values.get_raw_json();
json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{});
size_t hotkey_index = 0;
// Helper lambda to extract hotkey from JSON properties
auto extract_hotkey = [&](const wchar_t* property_name, const wchar_t* hotkey_name, bool default_win, bool default_ctrl, bool default_alt, bool default_shift, unsigned char default_key) -> Hotkey {
if (properties_json.HasKey(property_name))
{
try
{
json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name);
// Extract hotkey properties directly from JSON
bool win = hotkey_json.GetNamedBoolean(L"win", default_win);
bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", default_ctrl);
bool alt = hotkey_json.GetNamedBoolean(L"alt", default_alt);
bool shift = hotkey_json.GetNamedBoolean(L"shift", default_shift);
unsigned char key = static_cast<unsigned char>(
hotkey_json.GetNamedNumber(L"code", default_key));
return { win, ctrl, shift, alt, key, hotkey_name };
}
catch (...)
{
// If parsing individual hotkey fails, use defaults
return { default_win, default_ctrl, default_shift, default_alt, default_key, hotkey_name };
}
}
else
{
// Property doesn't exist, use defaults
return { default_win, default_ctrl, default_shift, default_alt, default_key, hotkey_name };
}
};
// Extract all hotkeys using the optimized helper
hotkeys[hotkey_index++] = extract_hotkey(L"ToggleEasyMouseShortcut", TOGGLEEASYMOUSE_SHORTCUT_NAME, false, true, false, true, 0x45); // Ctrl+Shift+E
hotkeys[hotkey_index++] = extract_hotkey(L"LockMachineShortcut", LOCKMACHINE_SHORTCUT_NAME, false, true, false, true, 0x4C); // Ctrl+Shift+L
hotkeys[hotkey_index++] = extract_hotkey(L"ReconnectShortcut", RECONNECT_SHORTCUT_NAME, true, true, true, false, 0x52); // Win+Ctrl+Alt+R
hotkeys[hotkey_index++] = extract_hotkey(L"Switch2AllPCShortcut", SWITCH2ALLPC_SHORTCUT_NAME, false, false, false, false, 0); // Disabled by default
}
catch (std::exception&)
{
// If settings file doesn't exist or is corrupted, use default hotkeys
size_t hotkey_index = 0;
hotkeys[hotkey_index++] = { false, true, false, true, 0x45, TOGGLEEASYMOUSE_SHORTCUT_NAME }; // Ctrl+Shift+E
hotkeys[hotkey_index++] = { false, true, false, true, 0x4C, LOCKMACHINE_SHORTCUT_NAME }; // Ctrl+Shift+L
hotkeys[hotkey_index++] = { true, true, true, false, 0x52, RECONNECT_SHORTCUT_NAME }; // Win+Ctrl+Alt+R
hotkeys[hotkey_index++] = CreateDisabledHotkey(SWITCH2ALLPC_SHORTCUT_NAME); // Disabled
}
}
return num_hotkeys;
}
void launch_add_firewall_process()
{
Logger::trace(L"Starting Process to add firewall rule");

View File

@@ -44,6 +44,8 @@ namespace
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
const wchar_t JSON_KEY_NAME[] = L"hotkeyName";
const wchar_t ACTIVATION_SHORTCUT_NAME[] = L"ActivationShortcut";
}
struct ModuleSettings
@@ -85,6 +87,7 @@ private:
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
catch (...)
{
@@ -104,6 +107,7 @@ private:
m_hotkey.shift = true;
m_hotkey.ctrl = false;
m_hotkey.key = 'T';
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
}

View File

@@ -12,6 +12,11 @@
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/EventWaiter.h>
namespace
{
const wchar_t ACTIVATION_SHORTCUT_NAME[] = L"OpenShortcutGuide";
}
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD /*ul_reason_for_call*/, LPVOID /*lpReserved*/)
{
return TRUE;
@@ -370,6 +375,11 @@ private:
m_hotkey.modifiersMask = MOD_SHIFT | MOD_WIN;
m_hotkey.vkCode = VK_OEM_2;
}
if (m_hotkey.name == nullptr)
{
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
}
};

View File

@@ -1,4 +1,4 @@
<Page
<Page
x:Class="WorkspacesEditor.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -172,7 +172,7 @@
VerticalContentAlignment="Stretch"
VerticalScrollBarVisibility="Auto"
Visibility="{Binding IsWorkspacesViewEmpty, Mode=OneWay, Converter={StaticResource BooleanToInvertedVisibilityConverter}, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl ItemsSource="{Binding WorkspacesView, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl x:Name="WorkspacesItemsControl" ItemsSource="{Binding WorkspacesView, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
@@ -235,7 +235,10 @@
Grid.Column="1"
Margin="12,12,12,12"
Orientation="Vertical">
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<StackPanel
x:Name="WorkspaceActionGroup"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
x:Name="MoreButton"
HorizontalAlignment="Right"
@@ -248,7 +251,8 @@
</Button>
<Popup
AllowsTransparency="True"
IsOpen="{Binding IsPopupVisible, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Closed="PopupClosed"
IsOpen="{Binding IsPopupVisible, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Placement="Left"
PlacementTarget="{Binding ElementName=MoreButton}"
StaysOpen="False">

View File

@@ -4,7 +4,8 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using ManagedCommon;
using WorkspacesEditor.Models;
using WorkspacesEditor.ViewModels;
@@ -42,17 +43,28 @@ namespace WorkspacesEditor
e.Handled = true;
Button button = sender as Button;
Project selectedProject = button.DataContext as Project;
selectedProject.IsPopupVisible = false;
_mainViewModel.DeleteProject(selectedProject);
}
private void MoreButton_Click(object sender, RoutedEventArgs e)
{
_mainViewModel.CloseAllPopups();
e.Handled = true;
Button button = sender as Button;
Project project = button.DataContext as Project;
project.IsPopupVisible = true;
}
private void PopupClosed(object sender, object e)
{
if (sender is Popup p && p.DataContext is Project proj)
{
proj.IsPopupVisible = false;
}
}
private void LaunchButton_Click(object sender, RoutedEventArgs e)
{
e.Handled = true;

View File

@@ -437,7 +437,11 @@ namespace WorkspacesEditor.ViewModels
private void RunSnapshotTool(bool isExistingProjectLaunched)
{
Process process = new Process();
process.StartInfo = new ProcessStartInfo(@".\PowerToys.WorkspacesSnapshotTool.exe");
var exeDir = Path.GetDirectoryName(Environment.ProcessPath);
var snapshotUtilsPath = Path.Combine(exeDir, "PowerToys.WorkspacesSnapshotTool.exe");
process.StartInfo = new ProcessStartInfo(snapshotUtilsPath);
process.StartInfo.CreateNoWindow = true;
process.StartInfo.Arguments = isExistingProjectLaunched ? $"{(int)InvokePoint.LaunchAndEdit}" : string.Empty;

View File

@@ -51,6 +51,8 @@
MouseLeave="AppBorder_MouseLeave">
<Expander
Margin="0,0,20,0"
AutomationProperties.AutomationId="{Binding AppName}"
AutomationProperties.Name="{Binding AppName}"
FlowDirection="RightToLeft"
IsEnabled="{Binding IsIncluded, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
IsExpanded="{Binding IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
@@ -399,7 +401,11 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ItemsControl ItemTemplateSelector="{StaticResource AppListDataTemplateSelector}" ItemsSource="{Binding ApplicationsListed, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
<ItemsControl
x:Name="CapturedAppList"
AutomationProperties.Name="Captured Application List"
ItemTemplateSelector="{StaticResource AppListDataTemplateSelector}"
ItemsSource="{Binding ApplicationsListed, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</StackPanel>
</ScrollViewer>

View File

@@ -0,0 +1,605 @@
// 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.Diagnostics;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace WorkspacesEditorUITest;
[TestClass]
[Ignore("not stable")]
public class WorkspacesEditingPageTests : WorkspacesUiAutomationBase
{
public WorkspacesEditingPageTests()
: base()
{
}
[TestMethod("WorkspacesEditingPage.RemoveApp")]
[TestCategory("Workspaces Editing Page UI")]
public void TestRemoveAppFromWorkspace()
{
// Find app list
var appList = Find<Custom>("AppList");
var initialAppCount = appList.FindAll<Custom>(By.ClassName("AppItem")).Count;
if (initialAppCount == 0)
{
Assert.Inconclusive("No apps in workspace to remove");
return;
}
// Remove first app
var firstApp = appList.FindAll<Custom>(By.ClassName("AppItem"))[0];
var appName = firstApp.GetAttribute("Name");
var removeButton = firstApp.Find<Button>("Remove");
removeButton.Click();
Thread.Sleep(500);
// Verify app removed from list
var finalAppCount = appList.FindAll<Custom>(By.ClassName("AppItem")).Count;
Assert.AreEqual(initialAppCount - 1, finalAppCount, "App should be removed from list");
// Verify preview updated
var previewPane = Find<Pane>("Preview");
var windowPreviews = previewPane.FindAll<Custom>(By.ClassName("WindowPreview"));
Assert.AreEqual(finalAppCount, windowPreviews.Count, "Preview should show correct number of windows");
}
[TestMethod("WorkspacesEditingPage.RemoveAndAddBackApp")]
[TestCategory("Workspaces Editing Page UI")]
public void TestRemoveAndAddBackApp()
{
// Find app list
var appList = Find<Custom>("AppList");
var apps = appList.FindAll<Custom>(By.ClassName("AppItem"));
if (apps.Count == 0)
{
Assert.Inconclusive("No apps in workspace to test");
return;
}
var firstApp = apps[0];
var appName = firstApp.GetAttribute("Name");
// Remove app
var removeButton = firstApp.Find<Button>("Remove");
removeButton.Click();
Thread.Sleep(500);
// Verify removed app shows in "removed apps" section
Assert.IsTrue(Has<Button>("Add back"), "Should have 'Add back' button for removed apps");
// Add back the app
var addBackButton = Find<Button>("Add back");
addBackButton.Click();
Thread.Sleep(500);
// Verify app is back in the list
var restoredApp = appList.Find<Custom>(By.Name(appName), timeoutMS: 2000);
Assert.IsNotNull(restoredApp, "App should be restored to the list");
// Verify preview updated
var previewPane = Find<Pane>("Preview");
var windowPreviews = previewPane.FindAll<Custom>(By.ClassName("WindowPreview"));
var currentAppCount = appList.FindAll<Custom>(By.ClassName("AppItem")).Count;
Assert.AreEqual(currentAppCount, windowPreviews.Count, "Preview should show all windows again");
}
[TestMethod("WorkspacesEditingPage.SetAppMinimized")]
[TestCategory("Workspaces Editing Page UI")]
public void TestSetAppMinimized()
{
// Find first app
var appList = Find<Custom>("AppList");
var apps = appList.FindAll<Custom>(By.ClassName("AppItem"));
if (apps.Count == 0)
{
Assert.Inconclusive("No apps in workspace to test");
return;
}
var firstApp = apps[0];
// Find and toggle minimized checkbox
var minimizedCheckbox = firstApp.Find<CheckBox>("Minimized");
bool wasMinimized = minimizedCheckbox.IsChecked;
minimizedCheckbox.Click();
Thread.Sleep(500);
// Verify state changed
Assert.AreNotEqual(wasMinimized, minimizedCheckbox.IsChecked, "Minimized state should toggle");
// Verify preview reflects the change
var previewPane = Find<Pane>("Preview");
var windowPreviews = previewPane.FindAll<Custom>(By.ClassName("WindowPreview"));
// The first window preview should indicate minimized state
if (minimizedCheckbox.IsChecked && windowPreviews.Count > 0)
{
var firstPreview = windowPreviews[0];
var opacity = firstPreview.GetAttribute("Opacity");
// Minimized windows might have reduced opacity or other visual indicator
Assert.IsNotNull(opacity, "Minimized window should have visual indication in preview");
}
}
[TestMethod("WorkspacesEditingPage.SetAppMaximized")]
[TestCategory("Workspaces Editing Page UI")]
public void TestSetAppMaximized()
{
// Find first app
var appList = Find<Custom>("AppList");
var apps = appList.FindAll<Custom>(By.ClassName("AppItem"));
if (apps.Count == 0)
{
Assert.Inconclusive("No apps in workspace to test");
return;
}
var firstApp = apps[0];
// Find and toggle maximized checkbox
var maximizedCheckbox = firstApp.Find<CheckBox>("Maximized");
bool wasMaximized = maximizedCheckbox.IsChecked;
maximizedCheckbox.Click();
Thread.Sleep(500);
// Verify state changed
Assert.AreNotEqual(wasMaximized, maximizedCheckbox.IsChecked, "Maximized state should toggle");
// Verify preview reflects the change
var previewPane = Find<Pane>("Preview");
if (maximizedCheckbox.IsChecked)
{
// Maximized window should fill the preview area
var windowPreviews = previewPane.FindAll<Custom>(By.ClassName("WindowPreview"));
if (windowPreviews.Count > 0)
{
var firstPreview = windowPreviews[0];
// Check if preview shows maximized state
var width = firstPreview.GetAttribute("Width");
var height = firstPreview.GetAttribute("Height");
Assert.IsNotNull(width, "Maximized window should have width in preview");
Assert.IsNotNull(height, "Maximized window should have height in preview");
}
}
}
[TestMethod("WorkspacesEditingPage.LaunchAsAdmin")]
[TestCategory("Workspaces Editing Page UI")]
public void TestSetLaunchAsAdmin()
{
// Find app that supports admin launch
var appList = Find<Custom>("AppList");
var apps = appList.FindAll<Custom>(By.ClassName("AppItem"));
bool foundAdminCapableApp = false;
foreach (var app in apps)
{
try
{
var adminCheckbox = app.Find<CheckBox>("Launch as admin", timeoutMS: 1000);
if (adminCheckbox != null && adminCheckbox.IsChecked)
{
foundAdminCapableApp = true;
bool wasAdmin = adminCheckbox.IsChecked;
adminCheckbox.Click();
Thread.Sleep(500);
// Verify state changed
Assert.AreNotEqual(wasAdmin, adminCheckbox.IsChecked, "Admin launch state should toggle");
break;
}
}
catch
{
// This app doesn't support admin launch
continue;
}
}
if (!foundAdminCapableApp)
{
Assert.Inconclusive("No apps in workspace support admin launch");
}
}
[TestMethod("WorkspacesEditingPage.AddCLIArgs")]
[TestCategory("Workspaces Editing Page UI")]
public void TestAddCommandLineArguments()
{
// Find first app
var appList = Find<Custom>("AppList");
var apps = appList.FindAll<Custom>(By.ClassName("AppItem"));
if (apps.Count == 0)
{
Assert.Inconclusive("No apps in workspace to test");
return;
}
var firstApp = apps[0];
// Find CLI args textbox
var cliArgsTextBox = firstApp.Find<TextBox>("Command line arguments", timeoutMS: 2000);
if (cliArgsTextBox == null)
{
Assert.Inconclusive("App does not support command line arguments");
return;
}
// Add test arguments
string testArgs = "--test-arg value";
cliArgsTextBox.SetText(testArgs);
Thread.Sleep(500);
// Verify arguments were entered
Assert.AreEqual(testArgs, cliArgsTextBox.Text, "Command line arguments should be set");
}
[TestMethod("WorkspacesEditingPage.ChangeAppPosition")]
[TestCategory("Workspaces Editing Page UI")]
public void TestManuallyChangeAppPosition()
{
// Find first app
var appList = Find<Custom>("AppList");
var apps = appList.FindAll<Custom>(By.ClassName("AppItem"));
if (apps.Count == 0)
{
Assert.Inconclusive("No apps in workspace to test");
return;
}
var firstApp = apps[0];
// Find position controls
var xPositionBox = firstApp.Find<TextBox>("X position", timeoutMS: 2000);
var yPositionBox = firstApp.Find<TextBox>("Y position", timeoutMS: 2000);
if (xPositionBox == null || yPositionBox == null)
{
// Try alternate approach with spinners
var positionSpinners = firstApp.FindAll<Custom>(By.ClassName("SpinBox"));
if (positionSpinners.Count >= 2)
{
xPositionBox = positionSpinners[0].Find<TextBox>(By.ClassName("TextBox"));
yPositionBox = positionSpinners[1].Find<TextBox>(By.ClassName("TextBox"));
}
}
if (xPositionBox != null && yPositionBox != null)
{
// Change position
xPositionBox.SetText("200");
Thread.Sleep(500);
yPositionBox.SetText("150");
Thread.Sleep(500);
// Verify preview updated
var previewPane = Find<Pane>("Preview");
var windowPreviews = previewPane.FindAll<Custom>(By.ClassName("WindowPreview"));
Assert.IsTrue(windowPreviews.Count > 0, "Preview should show window at new position");
}
else
{
Assert.Inconclusive("Could not find position controls");
}
}
[TestMethod("WorkspacesEditingPage.ChangeWorkspaceName")]
[TestCategory("Workspaces Editing Page UI")]
public void TestChangeWorkspaceName()
{
// Find workspace name textbox
var nameTextBox = Find<TextBox>("Workspace name");
string originalName = nameTextBox.Text;
// Change name
string newName = "Renamed_Workspace_" + DateTime.Now.Ticks;
nameTextBox.SetText(newName);
Thread.Sleep(500);
// Save changes
var saveButton = Find<Button>("Save");
saveButton.Click();
Thread.Sleep(1000);
// Verify we're back at main list
Assert.IsTrue(Has<Custom>("WorkspacesList"), "Should return to main list after saving");
// Verify workspace was renamed
var workspacesList = Find<Custom>("WorkspacesList");
var renamedWorkspace = workspacesList.Find<Custom>(By.Name(newName), timeoutMS: 2000);
Assert.IsNotNull(renamedWorkspace, "Workspace should be renamed in the list");
}
[TestMethod("WorkspacesEditingPage.SaveAndCancelWork")]
[TestCategory("Workspaces Editing Page UI")]
public void TestSaveAndCancelButtons()
{
// Make a change
var nameTextBox = Find<TextBox>("Workspace name");
string originalName = nameTextBox.Text;
string tempName = originalName + "_temp";
nameTextBox.SetText(tempName);
Thread.Sleep(500);
// Test Cancel button
var cancelButton = Find<Button>("Cancel");
cancelButton.Click();
Thread.Sleep(1000);
// Verify returned to main list without saving
Assert.IsTrue(Has<Custom>("WorkspacesList"), "Should return to main list");
// Go back to editing
var workspacesList = Find<Custom>("WorkspacesList");
var workspace = workspacesList.FindAll<Custom>(By.ClassName("WorkspaceItem"))[0];
workspace.Click();
Thread.Sleep(1000);
// Verify name wasn't changed
nameTextBox = Find<TextBox>("Workspace name");
Assert.AreEqual(originalName, nameTextBox.Text, "Name should not be changed after cancel");
// Now test Save button
nameTextBox.SetText(tempName);
Thread.Sleep(500);
var saveButton = Find<Button>("Save");
saveButton.Click();
Thread.Sleep(1000);
// Verify saved
workspacesList = Find<Custom>("WorkspacesList");
var savedWorkspace = workspacesList.Find<Custom>(By.Name(tempName), timeoutMS: 2000);
Assert.IsNotNull(savedWorkspace, "Workspace should be saved with new name");
}
[TestMethod("WorkspacesEditingPage.NavigateWithoutSaving")]
[TestCategory("Workspaces Editing Page UI")]
public void TestNavigateToMainPageWithoutSaving()
{
// Make a change
var nameTextBox = Find<TextBox>("Workspace name");
string originalName = nameTextBox.Text;
nameTextBox.SetText(originalName + "_unsaved");
Thread.Sleep(500);
// Click on "Workspaces" navigation/breadcrumb
if (Has<NavigationViewItem>("Workspaces", timeoutMS: 1000))
{
var workspacesNav = Find<NavigationViewItem>("Workspaces");
workspacesNav.Click();
Thread.Sleep(1000);
}
else if (Has<HyperlinkButton>("Workspaces", timeoutMS: 1000))
{
var workspacesBreadcrumb = Find<HyperlinkButton>("Workspaces");
workspacesBreadcrumb.Click();
Thread.Sleep(1000);
}
// If there's a confirmation dialog, handle it
if (Has<Button>("Discard", timeoutMS: 1000))
{
Find<Button>("Discard").Click();
Thread.Sleep(500);
}
// Verify returned to main list
Assert.IsTrue(Has<Custom>("WorkspacesList"), "Should return to main list");
// Verify changes weren't saved
var workspacesList = Find<Custom>("WorkspacesList");
var unsavedWorkspace = workspacesList.Find<Custom>(By.Name(originalName + "_unsaved"), timeoutMS: 1000);
Assert.IsNull(unsavedWorkspace, "Unsaved changes should not persist");
}
[TestMethod("WorkspacesEditingPage.CreateDesktopShortcut")]
[TestCategory("Workspaces Editing Page UI")]
public void TestCreateDesktopShortcut()
{
// Find desktop shortcut checkbox
var shortcutCheckbox = Find<CheckBox>("Create desktop shortcut");
// Get desktop path
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
// Get workspace name to check for shortcut
var nameTextBox = Find<TextBox>("Workspace name");
string workspaceName = nameTextBox.Text;
string shortcutPath = Path.Combine(desktopPath, $"{workspaceName}.lnk");
// Clean up any existing shortcut
if (File.Exists(shortcutPath))
{
File.Delete(shortcutPath);
Thread.Sleep(500);
}
// Check the checkbox
if (!shortcutCheckbox.IsChecked)
{
shortcutCheckbox.Click();
Thread.Sleep(500);
}
// Save
var saveButton = Find<Button>("Save");
saveButton.Click();
Thread.Sleep(2000); // Give time for shortcut creation
// Verify shortcut was created
Assert.IsTrue(File.Exists(shortcutPath), "Desktop shortcut should be created");
// Clean up
if (File.Exists(shortcutPath))
{
File.Delete(shortcutPath);
}
}
[TestMethod("WorkspacesEditingPage.DesktopShortcutState")]
[TestCategory("Workspaces Editing Page UI")]
public void TestDesktopShortcutCheckboxState()
{
// Get workspace name
var nameTextBox = Find<TextBox>("Workspace name");
string workspaceName = nameTextBox.Text;
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string shortcutPath = Path.Combine(desktopPath, $"{workspaceName}.lnk");
// Find checkbox
var shortcutCheckbox = Find<CheckBox>("Create desktop shortcut");
// Test 1: When shortcut exists
if (!File.Exists(shortcutPath))
{
// Create shortcut first
if (!shortcutCheckbox.IsChecked)
{
shortcutCheckbox.Click();
Thread.Sleep(500);
}
Find<Button>("Save").Click();
Thread.Sleep(2000);
// Navigate back to editing
NavigateToEditingPage();
}
shortcutCheckbox = Find<CheckBox>("Create desktop shortcut");
Assert.IsTrue(shortcutCheckbox.IsChecked, "Checkbox should be checked when shortcut exists");
// Test 2: Remove shortcut
if (File.Exists(shortcutPath))
{
File.Delete(shortcutPath);
Thread.Sleep(500);
}
// Re-navigate to refresh state
Find<Button>("Cancel").Click();
Thread.Sleep(1000);
NavigateToEditingPage();
shortcutCheckbox = Find<CheckBox>("Create desktop shortcut");
Assert.IsFalse(shortcutCheckbox.IsChecked, "Checkbox should be unchecked when shortcut doesn't exist");
}
[TestMethod("WorkspacesEditingPage.LaunchAndEdit")]
[TestCategory("Workspaces Editing Page UI")]
public void TestLaunchAndEditCapture()
{
// Find Launch and Edit button
var launchEditButton = Find<Button>("Launch and Edit");
launchEditButton.Click();
Thread.Sleep(3000); // Wait for apps to launch
// Open a new application
Process.Start("calc.exe");
Thread.Sleep(2000);
// Click Capture
var captureButton = Find<Button>("Capture");
captureButton.Click();
Thread.Sleep(2000);
// Verify new app was added
var appList = Find<Custom>("AppList");
var apps = appList.FindAll<Custom>(By.ClassName("AppItem"));
bool foundCalculator = false;
foreach (var app in apps)
{
var appName = app.GetAttribute("Name");
if (appName.Contains("Calculator", StringComparison.OrdinalIgnoreCase))
{
foundCalculator = true;
break;
}
}
Assert.IsTrue(foundCalculator, "Newly opened Calculator should be captured and added");
// Clean up
foreach (var process in Process.GetProcessesByName("CalculatorApp"))
{
process.Kill();
}
foreach (var process in Process.GetProcessesByName("Calculator"))
{
process.Kill();
}
}
// Helper methods
private void NavigateToEditingPage()
{
// Ensure we have at least one workspace
if (!Has<Custom>("WorkspacesList", timeoutMS: 1000))
{
CreateTestWorkspace();
}
// Click on first workspace to edit
var workspacesList = Find<Custom>("WorkspacesList");
var workspaceItems = workspacesList.FindAll<Custom>(By.ClassName("WorkspaceItem"));
if (workspaceItems.Count == 0)
{
CreateTestWorkspace();
workspaceItems = workspacesList.FindAll<Custom>(By.ClassName("WorkspaceItem"));
}
workspaceItems[0].Click();
Thread.Sleep(1000);
}
private void CreateTestWorkspace()
{
// Open a test app
Process.Start("notepad.exe");
Thread.Sleep(1000);
// Create workspace
var createButton = Find<Button>("Create Workspace");
createButton.Click();
Thread.Sleep(1000);
// Capture
var captureButton = Find<Button>("Capture");
captureButton.Click();
Thread.Sleep(2000);
// Save with default name
var saveButton = Find<Button>("Save");
saveButton.Click();
Thread.Sleep(1000);
// Close test app
foreach (var process in Process.GetProcessesByName("notepad"))
{
process.Kill();
}
}
}

View File

@@ -8,10 +8,10 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace WorkspacesEditorUITest;
[TestClass]
public class WorkspacesEditorTests : UITestBase
public class WorkspacesEditorTests : WorkspacesUiAutomationBase
{
public WorkspacesEditorTests()
: base(PowerToysModule.Workspaces, WindowSize.Medium)
: base()
{
}
@@ -21,4 +21,217 @@ public class WorkspacesEditorTests : UITestBase
{
Assert.IsTrue(this.Has<Button>("Create Workspace"), "Should have create workspace button");
}
/*
[TestMethod("WorkspacesEditor.Editor.NewWorkspaceAppearsInList")]
[TestCategory("Workspaces UI")]
public void TestNewWorkspaceAppearsInListAfterCapture()
{
// Open test application
OpenNotepad();
Thread.Sleep(2000);
// Create workspace
var createButton = Find<Button>("Create Workspace");
createButton.Click();
Thread.Sleep(1000);
// Capture
var captureButton = Find<Button>("Capture");
captureButton.Click();
Thread.Sleep(2000);
// Save workspace
var saveButton = Find<Button>("Save");
saveButton.Click();
Thread.Sleep(1000);
// Verify workspace appears in list
var workspacesList = Find<Custom>("WorkspacesList");
var workspaceItems = workspacesList.FindAll<Custom>(By.ClassName("WorkspaceItem"));
Assert.IsTrue(workspaceItems.Count > 0, "New workspace should appear in the list");
CloseNotepad();
}
[TestMethod("WorkspacesEditor.Editor.CancelCaptureDoesNotAddWorkspace")]
[TestCategory("Workspaces UI")]
public void TestCancelCaptureDoesNotAddWorkspace()
{
// Count existing workspaces
var workspacesList = Find<Custom>("WorkspacesList");
var initialCount = workspacesList.FindAll<Custom>(By.ClassName("WorkspaceItem")).Count;
// Create workspace
var createButton = Find<Button>("Create Workspace");
createButton.Click();
Thread.Sleep(1000);
// Cancel
var cancelButton = Find<Button>("Cancel");
cancelButton.Click();
Thread.Sleep(1000);
// Verify count hasn't changed
var finalCount = workspacesList.FindAll<Custom>(By.ClassName("WorkspaceItem")).Count;
Assert.AreEqual(initialCount, finalCount, "Workspace count should not change after canceling");
}
[TestMethod("WorkspacesEditor.Editor.SearchFiltersWorkspaces")]
[TestCategory("Workspaces UI")]
public void TestSearchFiltersWorkspaces()
{
// Create test workspaces first
CreateTestWorkspace("TestWorkspace1");
CreateTestWorkspace("TestWorkspace2");
CreateTestWorkspace("DifferentName");
// Find search box
var searchBox = Find<TextBox>("Search");
searchBox.SetText("TestWorkspace");
Thread.Sleep(1000);
// Verify filtered results
var workspacesList = Find<Custom>("WorkspacesList");
var visibleItems = workspacesList.FindAll<Custom>(By.ClassName("WorkspaceItem"));
// Should only show items matching "TestWorkspace"
Assert.IsTrue(visibleItems.Count >= 2, "Should show at least 2 TestWorkspace items");
// Clear search
searchBox.SetText(string.Empty);
Thread.Sleep(500);
}
[TestMethod("WorkspacesEditor.Editor.SortByWorks")]
[TestCategory("Workspaces UI")]
public void TestSortByFunctionality()
{
// Find sort dropdown
var sortDropdown = Find<ComboBox>("SortBy");
sortDropdown.Click();
Thread.Sleep(500);
// Select different sort options
var sortOptions = FindAll<Custom>(By.ClassName("ComboBoxItem"));
if (sortOptions.Count > 1)
{
sortOptions[1].Click(); // Select second option
Thread.Sleep(1000);
// Verify list is updated (we can't easily verify sort order in UI tests)
var workspacesList = Find<Custom>("WorkspacesList");
Assert.IsNotNull(workspacesList, "Workspaces list should still be visible after sorting");
}
}
[TestMethod("WorkspacesEditor.Editor.SortByPersists")]
[TestCategory("Workspaces UI")]
public void TestSortByPersistsAfterRestart()
{
// Set sort option
var sortDropdown = Find<ComboBox>("SortBy");
sortDropdown.Click();
Thread.Sleep(500);
var sortOptions = FindAll<Custom>(By.ClassName("ComboBoxItem"));
string selectedOption = string.Empty;
if (sortOptions.Count > 1)
{
sortOptions[1].Click(); // Select second option
selectedOption = sortDropdown.Text;
Thread.Sleep(1000);
}
// Restart editor
RestartScopeExe();
Thread.Sleep(2000);
// Verify sort option persisted
sortDropdown = Find<ComboBox>("SortBy");
Assert.AreEqual(selectedOption, sortDropdown.Text, "Sort option should persist after restart");
}
[TestMethod("WorkspacesEditor.Editor.RemoveWorkspace")]
[TestCategory("Workspaces UI")]
public void TestRemoveWorkspace()
{
// Create a test workspace
CreateTestWorkspace("WorkspaceToRemove");
// Find the workspace in the list
var workspacesList = Find<Custom>("WorkspacesList");
var workspaceItem = workspacesList.Find<Custom>(By.Name("WorkspaceToRemove"));
// Click remove button
var removeButton = workspaceItem.Find<Button>("Remove");
removeButton.Click();
Thread.Sleep(1000);
// Confirm removal if dialog appears
if (Has<Button>("Yes"))
{
Find<Button>("Yes").Click();
Thread.Sleep(1000);
}
// Verify workspace is removed
Assert.IsFalse(Has(By.Name("WorkspaceToRemove")), "Workspace should be removed from list");
}
[TestMethod("WorkspacesEditor.Editor.EditOpensEditingPage")]
[TestCategory("Workspaces UI")]
public void TestEditOpensEditingPage()
{
// Create a test workspace if none exist
if (!Has<Custom>("WorkspacesList"))
{
CreateTestWorkspace("TestWorkspace");
}
// Find first workspace
var workspacesList = Find<Custom>("WorkspacesList");
var workspaceItem = workspacesList.FindAll<Custom>(By.ClassName("WorkspaceItem"))[0];
// Click edit button
var editButton = workspaceItem.Find<Button>("Edit");
editButton.Click();
Thread.Sleep(1000);
// Verify editing page opened
Assert.IsTrue(Has<Button>("Save"), "Should have Save button on editing page");
Assert.IsTrue(Has<Button>("Cancel"), "Should have Cancel button on editing page");
// Go back
Find<Button>("Cancel").Click();
Thread.Sleep(1000);
}
[TestMethod("WorkspacesEditor.Editor.ClickWorkspaceOpensEditingPage")]
[TestCategory("Workspaces UI")]
public void TestClickWorkspaceOpensEditingPage()
{
// Create a test workspace if none exist
if (!Has<Custom>("WorkspacesList"))
{
CreateTestWorkspace("TestWorkspace");
}
// Find first workspace
var workspacesList = Find<Custom>("WorkspacesList");
var workspaceItem = workspacesList.FindAll<Custom>(By.ClassName("WorkspaceItem"))[0];
// Click on the workspace item itself
workspaceItem.Click();
Thread.Sleep(1000);
// Verify editing page opened
Assert.IsTrue(Has<Button>("Save"), "Should have Save button on editing page");
Assert.IsTrue(Has<Button>("Cancel"), "Should have Cancel button on editing page");
// Go back
Find<Button>("Cancel").Click();
Thread.Sleep(1000);
}
*/
}

View File

@@ -23,6 +23,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
<ProjectReference Include="..\WorkspacesEditor\WorkspacesEditor.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace WorkspacesEditorUITest;
[TestClass]
[Ignore("NOT STABLE")]
public class WorkspacesLauncherTest : WorkspacesUiAutomationBase
{
public WorkspacesLauncherTest()
: base()
{
}
[TestMethod("WorkspacesEditor.Launcher.LaunchFromEditor")]
[TestCategory("Workspaces UI")]
public void TestLaunchWorkspaceFromEditor()
{
ClearWorkspaces();
var uuid = Guid.NewGuid().ToString("N").Substring(0, 8);
CreateTestWorkspace(uuid);
CloseNotepad();
var launchButton = Find<Button>(By.Name("Launch"));
launchButton.Click();
Task.Delay(2000).Wait();
var processes = System.Diagnostics.Process.GetProcessesByName("notepad");
Assert.IsTrue(processes?.Length > 0);
}
[TestMethod("WorkspacesEditor.Launcher.CancelLaunch")]
[TestCategory("Workspaces UI")]
public void TestCancelLaunch()
{
// Create workspace with multiple apps
CreateWorkspaceWithApps();
// Launch workspace
var workspacesList = Find<Custom>("WorkspacesList");
var workspaceItem = workspacesList.FindAll<Custom>(By.ClassName("WorkspaceItem"))[0];
var launchButton = workspaceItem.Find<Button>("Launch");
launchButton.Click();
Thread.Sleep(1000);
// Cancel launch
if (Has<Button>("Cancel launch"))
{
Find<Button>("Cancel launch").Click();
Thread.Sleep(1000);
// Verify launcher closed
Assert.IsFalse(Has<Window>("Workspaces Launcher"), "Launcher window should close after cancel");
}
// Close any apps that may have launched
CloseTestApplications();
}
[TestMethod("WorkspacesEditor.Launcher.DismissKeepsLaunching")]
[TestCategory("Workspaces UI")]
public void TestDismissKeepsAppsLaunching()
{
// Create workspace with apps
CreateWorkspaceWithApps();
// Launch workspace
var workspacesList = Find<Custom>("WorkspacesList");
var workspaceItem = workspacesList.FindAll<Custom>(By.ClassName("WorkspaceItem"))[0];
var launchButton = workspaceItem.Find<Button>("Launch");
launchButton.Click();
Thread.Sleep(1000);
// Dismiss launcher
if (Has<Button>("Dismiss"))
{
Find<Button>("Dismiss").Click();
Thread.Sleep(1000);
// Verify launcher closed but apps continue launching
Assert.IsFalse(Has<Window>("Workspaces Launcher"), "Launcher window should close after dismiss");
// Wait for apps to finish launching
Thread.Sleep(3000);
// Verify apps launched (notepad should be open)
Assert.IsTrue(WindowHelper.IsWindowOpen("Notepad"), "Apps should continue launching after dismiss");
}
// Close launched apps
CloseTestApplications();
}
}

View File

@@ -0,0 +1,195 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace WorkspacesEditorUITest;
[TestClass]
public class WorkspacesSettingsTests : UITestBase
{
public WorkspacesSettingsTests()
: base(PowerToysModule.PowerToysSettings, WindowSize.Medium)
{
}
[TestMethod("WorkspacesSettings.LaunchFromSettings")]
[TestCategory("Workspaces Settings UI")]
public void TestLaunchEditorFromSettingsPage()
{
GoToSettingsPageAndEnable();
}
[TestMethod("WorkspacesSettings.ActivationShortcut")]
[TestCategory("Workspaces Settings UI")]
public void TestActivationShortcutCustomization()
{
GoToSettingsPageAndEnable();
// Find the activation shortcut control
var shortcutButton = Find<Button>("Activation shortcut");
Assert.IsNotNull(shortcutButton, "Activation shortcut control should exist");
// Test customizing the shortcut
shortcutButton.Click();
Task.Delay(1000).Wait();
// Send new key combination (Win+Ctrl+W)
SendKeys(Key.Win, Key.Ctrl, Key.W);
var saveButton = Find<Button>("Save");
Assert.IsNotNull(saveButton, "Save button should exist after editing shortcut");
saveButton.Click();
var helpText = shortcutButton.HelpText;
Assert.AreEqual("Win + Ctrl + W", helpText, "Activation shortcut should be updated to Win + Ctrl + W");
}
[TestMethod("WorkspacesSettings.EnableToggle")]
[TestCategory("Workspaces Settings UI")]
public void TestEnableDisableModule()
{
GoToSettingsPageAndEnable();
// Find the enable toggle
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
Assert.IsNotNull(enableToggle, "Enable Workspaces toggle should exist");
Assert.IsTrue(enableToggle.IsOn, "Enable Workspaces toggle should be in the 'on' state");
// Toggle the state
enableToggle.Click();
Task.Delay(500).Wait();
// Verify state changed
Assert.IsFalse(enableToggle.IsOn, "Toggle state should change");
// Verify related controls are enabled/disabled accordingly
var launchButton = Find<Button>("Launch editor");
Assert.IsFalse(launchButton.Enabled, "Launch editor button should be disabled when module is disabled");
}
[TestMethod("WorkspacesSettings.LaunchByActivationShortcut")]
[TestCategory("Workspaces Settings UI")]
[Ignore("Wait until settings & runner can be connected in framework")]
public void TestLaunchEditorByActivationShortcut()
{
// Ensure module is enabled
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
if (!enableToggle.IsOn)
{
enableToggle.Click();
Thread.Sleep(500);
}
// Close settings window to test shortcut
ExitScopeExe();
Thread.Sleep(1000);
// Default shortcut is Win+Ctrl+`
SendKeys(Key.Win, Key.Ctrl, Key.W);
Thread.Sleep(2000);
// Verify editor opened
Assert.IsTrue(WindowHelper.IsWindowOpen("Workspaces Editor"), "Workspaces Editor should open with activation shortcut");
// Reopen settings for next tests
RestartScopeExe();
NavigateToWorkspacesSettings();
}
[TestMethod("WorkspacesSettings.DisableModuleNoLaunch")]
[TestCategory("Workspaces Settings UI")]
[Ignore("Wait until settings & runner can be connected in framework")]
public void TestDisabledModuleDoesNotLaunchByShortcut()
{
// Disable the module
var enableToggle = Find<ToggleSwitch>("Enable Workspaces");
if (enableToggle.IsOn)
{
enableToggle.Click();
Thread.Sleep(500);
}
// Close settings to test shortcut
ExitScopeExe();
Thread.Sleep(1000);
// Try to launch with shortcut
SendKeys(Key.Win, Key.Ctrl, Key.W);
Thread.Sleep(2000);
// Verify editor did not open
Assert.IsFalse(WindowHelper.IsWindowOpen("Workspaces Editor"), "Workspaces Editor should not open when module is disabled");
// Reopen settings and re-enable the module
RestartScopeExe();
NavigateToWorkspacesSettings();
enableToggle = Find<ToggleSwitch>("Enable Workspaces");
if (!enableToggle.IsOn)
{
enableToggle.Click();
Thread.Sleep(500);
}
}
[TestMethod("WorkspacesSettings.QuickAccessLaunch")]
[TestCategory("Workspaces Settings UI")]
[Ignore("Wait until tray icon supported is in framework")]
public void TestLaunchFromQuickAccess()
{
// This test verifies the "quick access" mention in settings
// Look for any quick access related UI elements
var quickAccessInfo = FindAll(By.LinkText("quick access"));
if (quickAccessInfo.Count > 0)
{
Assert.IsTrue(quickAccessInfo.Count > 0, "Quick access information should be present in settings");
}
// Note: Actual system tray/quick access interaction would require
// more complex automation that might be platform-specific
}
private void NavigateToWorkspacesSettings()
{
// Find and click Workspaces in the navigation
var workspacesNavItem = Find<NavigationViewItem>("Workspaces");
workspacesNavItem.Click();
Thread.Sleep(1000);
}
private void GoToSettingsPageAndEnable()
{
if (this.FindAll<NavigationViewItem>("Workspaces").Count == 0)
{
this.Find<NavigationViewItem>("Windowing & Layouts").Click();
}
this.Find<NavigationViewItem>("Workspaces").Click();
var enableButton = this.Find<ToggleSwitch>("Enable Workspaces");
Assert.IsNotNull(enableButton, "Enable Workspaces toggle should exist");
if (!enableButton.IsOn)
{
enableButton.Click();
Task.Delay(500).Wait(); // Wait for the toggle animation and state change
}
// Verify it's now enabled
Assert.IsTrue(enableButton.IsOn, "Enable Workspaces toggle should be in the 'on' state");
}
private void AttachWorkspacesEditor()
{
Task.Delay(200).Wait();
this.Session.Attach(PowerToysModule.Workspaces);
}
}

View File

@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using WorkspacesEditor.Utils;
namespace WorkspacesEditorUITest;
[TestClass]
public class WorkspacesSnapshotTests : WorkspacesUiAutomationBase
{
public WorkspacesSnapshotTests()
: base()
{
}
[TestMethod("WorkspacesSnapshot.CancelCapture")]
[TestCategory("Workspaces Snapshot UI")]
public void TestCaptureCancel()
{
AttachWorkspacesEditor();
var createButton = Find<Button>("Create Workspace");
createButton.Click();
Task.Delay(1000).Wait();
AttachSnapshotWindow();
var cancelButton = Find<Button>("Cancel");
Assert.IsNotNull(cancelButton, "Capture button should exist");
cancelButton.Click();
}
[TestMethod("WorkspacesSnapshot.CapturePackagedApps")]
[TestCategory("Workspaces Snapshot UI")]
public void TestCapturePackagedApplications()
{
OpenCalculator();
// OpenWindowsSettings();
Task.Delay(2000).Wait();
AttachWorkspacesEditor();
var createButton = Find<Button>("Create Workspace");
createButton.Click();
Task.Delay(1000).Wait();
AttachSnapshotWindow();
var captureButton = Find<Button>("Capture");
captureButton.Click();
Task.Delay(3000).Wait();
// Verify captured windows by reading the temporary workspaces file as the ground truth.
var editorIO = new WorkspacesEditorIO();
var workspace = editorIO.ParseTempProject();
Assert.IsNotNull(workspace, "Workspace data should be deserialized.");
Assert.IsNotNull(workspace.Applications, "Workspace should contain a list of apps.");
bool isCalculatorFound = workspace.Applications.Any(app => app.AppPath.Contains("Calculator", StringComparison.OrdinalIgnoreCase));
// bool isSettingsFound = workspace.Applications.Any(app => app.AppPath.Contains("Settings", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(isCalculatorFound, "Calculator should be captured in the workspace data.");
// Assert.IsTrue(isSettingsFound, "Settings should be captured in the workspace data.");
// Cancel to clean up
AttachWorkspacesEditor();
Find<Button>("Cancel").Click();
Task.Delay(1000).Wait();
// Close test applications
CloseCalculator();
// CloseWindowsSettings();
}
}

View File

@@ -0,0 +1,253 @@
// 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.Diagnostics;
using Microsoft.PowerToys.UITest;
namespace WorkspacesEditorUITest
{
public class WorkspacesUiAutomationBase : UITestBase
{
public WorkspacesUiAutomationBase()
: base(PowerToysModule.Workspaces, WindowSize.Medium)
{
}
protected void CreateTestWorkspace(string name)
{
// Open notepad for capture
OpenNotepad();
Task.Delay(1000).Wait();
// Create workspace
AttachWorkspacesEditor();
var createButton = Find<Button>("Create Workspace");
createButton.Click();
Task.Delay(500).Wait();
// Capture
AttachSnapshotWindow();
var captureButton = Find<Button>("Capture");
captureButton.Click();
Task.Delay(5000).Wait();
// Set name
var nameTextBox = Find<TextBox>("EditNameTextBox");
nameTextBox.SetText(name);
// Save
Find<Button>("Save Workspace").Click();
// Close notepad
CloseNotepad();
}
// DO NOT USE UNTIL FRAMEWORK AVAILABLE, CAN'T FIND BUTTON FOR SECOND LOOP
protected void ClearWorkspaces()
{
while (true)
{
try
{
var root = Find<Element>(By.AccessibilityId("WorkspacesItemsControl"));
var buttons = root.FindAll<Button>(By.AccessibilityId("MoreButton"));
Debug.WriteLine($"Found {buttons.Count} button");
var button = buttons[^1];
button.Click();
Task.Delay(500).Wait();
var remove = Find<Button>(By.Name("Remove"));
remove.Click();
Task.Delay(500).Wait();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
break;
}
}
}
protected void CreateWorkspaceWithApps()
{
// Open multiple test applications
OpenTestApplications();
Thread.Sleep(3000);
// Create workspace
var createButton = Find<Button>("Create Workspace");
createButton.Click();
Thread.Sleep(1000);
// Capture
var captureButton = Find<Button>("Capture");
captureButton.Click();
Thread.Sleep(2000);
// Save
Find<Button>("Save").Click();
Thread.Sleep(1000);
// Close test applications
CloseTestApplications();
}
protected void OpenTestApplications()
{
OpenNotepad();
// Could add more applications here
Thread.Sleep(1000);
}
protected void CloseTestApplications()
{
CloseNotepad();
}
protected void CloseWorkspacesEditor()
{
// Find and close the Workspaces Editor window
if (WindowHelper.IsWindowOpen("Workspaces Editor"))
{
var editorWindow = Find<Window>("Workspaces Editor");
editorWindow.Close();
Thread.Sleep(1000);
}
}
protected void ResetShortcutToDefault(Custom shortcutControl)
{
// Right-click on the shortcut control to open context menu
shortcutControl.Click(rightClick: true);
Thread.Sleep(500);
// Look for a "Reset to default" or similar option in the context menu
try
{
// Try to find various possible menu item texts for reset option
var resetOption = Find("Reset to default");
resetOption?.Click();
}
catch
{
try
{
// Try alternative text
var resetOption = Find("Reset");
resetOption?.Click();
}
catch
{
try
{
// Try another alternative
var resetOption = Find("Default");
resetOption?.Click();
}
catch
{
// If context menu doesn't have reset option, try keyboard shortcut
// ESC to close any open menus first
SendKeys(Key.Esc);
Thread.Sleep(200);
// Click on the control and try to clear it with standard shortcuts
shortcutControl.Click();
Thread.Sleep(200);
// Try Ctrl+A to select all, then Delete to clear
SendKeys(Key.Ctrl, Key.A);
Thread.Sleep(100);
SendKeys(Key.Delete);
Thread.Sleep(500);
}
}
}
}
protected void OpenNotepad()
{
var process = System.Diagnostics.Process.Start("notepad.exe");
Task.Delay(1000).Wait();
}
protected void CloseNotepad()
{
var processes = System.Diagnostics.Process.GetProcessesByName("notepad");
foreach (var process in processes)
{
try
{
process.Kill();
process.WaitForExit();
}
catch
{
// ignore
}
}
}
private void AttachPowertoySetting()
{
Task.Delay(200).Wait();
this.Session.Attach(PowerToysModule.PowerToysSettings);
}
protected void AttachWorkspacesEditor()
{
Task.Delay(200).Wait();
this.Session.Attach(PowerToysModule.Workspaces);
}
protected void AttachSnapshotWindow()
{
Task.Delay(5000).Wait();
this.Session.Attach("Snapshot Creator");
}
protected void OpenCalculator()
{
Process.Start("calc.exe");
Task.Delay(1000).Wait();
}
protected void CloseCalculator()
{
foreach (var process in Process.GetProcessesByName("CalculatorApp"))
{
process.Kill();
}
foreach (var process in Process.GetProcessesByName("Calculator"))
{
process.Kill();
}
}
protected void OpenWindowsSettings()
{
Process.Start(new ProcessStartInfo
{
FileName = "ms-settings:",
UseShellExecute = true,
});
Task.Delay(500).Wait();
}
protected void CloseWindowsSettings()
{
foreach (var process in Process.GetProcessesByName("SystemSettings"))
{
process.Kill();
}
}
}
}

View File

@@ -31,6 +31,7 @@ namespace
const wchar_t JSON_KEY_RUN_SNAPSHOT_TOOL_HOTKEY[] = L"run-snapshot-tool-hotkey";
const wchar_t JSON_KEY_RUN_LAUNCHER_HOTKEY[] = L"run-launcher-hotkey";
const wchar_t JSON_KEY_VALUE[] = L"value";
const wchar_t ACTIVATION_HOTKEY_NAME[] = L"Hotkey";
}
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
@@ -307,6 +308,7 @@ private:
}
m_hotkey.vkCode = static_cast<WORD>(hotkey.get_code());
m_hotkey.name = ACTIVATION_HOTKEY_NAME;
}
}
}

View File

@@ -28,6 +28,8 @@ namespace
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_HOTKEY[] = L"hotkey";
const wchar_t JSON_KEY_VALUE[] = L"value";
const wchar_t JSON_KEY_NAME[] = L"hotkeyName";
const wchar_t ACTIVATION_SHORTCUT_NAME[] = L"Hotkey";
}
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
@@ -248,6 +250,7 @@ private:
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
catch (...)
{
@@ -267,6 +270,7 @@ private:
m_hotkey.shift = false;
m_hotkey.ctrl = true;
m_hotkey.key = 'T';
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
}

View File

@@ -124,7 +124,7 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties {
}
/// <summary>
/// Looks up a localized string similar to Info: Ending the Explorer process isn&apos;t possible..
/// Looks up a localized string similar to Info: Killing the Explorer process isn&apos;t possible..
/// </summary>
public static string windowwalker_ExplorerInfoTitle {
get {
@@ -133,7 +133,7 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties {
}
/// <summary>
/// Looks up a localized string similar to End task.
/// Looks up a localized string similar to Kill process.
/// </summary>
public static string windowwalker_Kill {
get {
@@ -142,7 +142,7 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties {
}
/// <summary>
/// Looks up a localized string similar to You are going to end the following process:.
/// Looks up a localized string similar to Your are going to kill the following process:.
/// </summary>
public static string windowwalker_KillMessage {
get {
@@ -160,7 +160,7 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties {
}
/// <summary>
/// Looks up a localized string similar to End task confirmation.
/// Looks up a localized string similar to Kill process confirmation.
/// </summary>
public static string windowwalker_KillMessageTitle {
get {

View File

@@ -39,6 +39,8 @@ namespace
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
const wchar_t JSON_KEY_NAME[] = L"hotkeyName";
const wchar_t ACTIVATION_SHORTCUT_NAME[] = L"ActivationShortcut";
}
struct ModuleSettings
@@ -82,6 +84,7 @@ private:
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
catch (...)
{
@@ -101,6 +104,7 @@ private:
m_hotkey.shift = true;
m_hotkey.ctrl = false;
m_hotkey.key = 'C';
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
}

View File

@@ -45,14 +45,41 @@ public:
bool shift = false;
bool alt = false;
unsigned char key = 0;
const wchar_t* name = nullptr;
std::strong_ordering operator<=>(const Hotkey&) const = default;
std::strong_ordering operator<=>(const Hotkey& other) const
{
// Compare bool fields first
if (auto cmp = (win <=> other.win); cmp != 0)
return cmp;
if (auto cmp = (ctrl <=> other.ctrl); cmp != 0)
return cmp;
if (auto cmp = (shift <=> other.shift); cmp != 0)
return cmp;
if (auto cmp = (alt <=> other.alt); cmp != 0)
return cmp;
// Compare key value only
return key <=> other.key;
// Note: Deliberately NOT comparing 'name' field
}
bool operator==(const Hotkey& other) const
{
return win == other.win &&
ctrl == other.ctrl &&
shift == other.shift &&
alt == other.alt &&
key == other.key;
}
};
struct HotkeyEx
{
WORD modifiersMask = 0;
WORD vkCode = 0;
const wchar_t* name = nullptr;
};
/* Returns the localized name of the PowerToy*/

View File

@@ -26,6 +26,8 @@ namespace
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_OPEN_POWERLAUNCHER[] = L"open_powerlauncher";
const wchar_t JSON_KEY_USE_CENTRALIZED_KEYBOARD_HOOK[] = L"use_centralized_keyboard_hook";
const wchar_t JSON_KEY_NAME[] = L"hotkeyName";
const wchar_t ACTIVATION_SHORTCUT_NAME[] = L"OpenPowerLauncher";
}
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
@@ -390,6 +392,7 @@ void Microsoft_Launcher::parse_hotkey(PowerToysSettings::PowerToyValues& setting
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
catch (...)
{
@@ -418,6 +421,7 @@ void Microsoft_Launcher::parse_hotkey(PowerToysSettings::PowerToyValues& setting
m_hotkey.shift = false;
m_hotkey.ctrl = false;
m_hotkey.key = VK_SPACE;
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
}

View File

@@ -42,6 +42,8 @@ namespace
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
const wchar_t JSON_KEY_ALWAYS_RUN_NOT_ELEVATED[] = L"AlwaysRunNotElevated";
const wchar_t JSON_KEY_NAME[] = L"hotkeyName";
const wchar_t ACTIVATION_SHORTCUT_NAME[] = L"ActivationShortcut";
}
// The PowerToy name that will be shown in the settings.
@@ -127,6 +129,7 @@ private:
m_hotkey.shift = false;
m_hotkey.ctrl = true;
m_hotkey.key = ' ';
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
void parse_hotkey(winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
@@ -138,6 +141,7 @@ private:
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
catch (...)
{
@@ -152,6 +156,7 @@ private:
m_hotkey.shift = false;
m_hotkey.ctrl = true;
m_hotkey.key = ' ';
m_hotkey.name = ACTIVATION_SHORTCUT_NAME;
}
}

View File

@@ -1,5 +1,6 @@
#include "pch.h"
#include "centralized_hotkeys.h"
#include "hotkey_conflict_detector.h"
#include <map>
#include <common/logger/logger.h>
@@ -40,12 +41,15 @@ namespace CentralizedHotkeys
return res;
}
bool AddHotkeyAction(Shortcut shortcut, Action action)
bool AddHotkeyAction(Shortcut shortcut, Action action, std::wstring moduleName, bool isEnabled)
{
if (!actions[shortcut].empty())
HotkeyConflictDetector::HotkeyConflictManager& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
HotkeyConflictDetector::Hotkey hotkey = HotkeyConflictDetector::ShortcutToHotkey(shortcut);
bool succeed = hkmng.AddHotkey(hotkey, moduleName.c_str(), shortcut.hotkeyName, isEnabled);
if (!succeed)
{
// It will only work if previous one is rewritten
Logger::warn(L"{} shortcut is already registered", ToWstring(shortcut));
Logger::warn(L"Shortcut conflict detected. Shortcut: {}, from module: {}", ToWstring(shortcut), moduleName);
}
actions[shortcut].push_back(action);
@@ -57,7 +61,6 @@ namespace CentralizedHotkeys
static int nextId = 0;
ids[shortcut] = nextId++;
}
if (!RegisterHotKey(runnerWindow, ids[shortcut], shortcut.modifiersMask, shortcut.vkCode))
{
Logger::warn(L"Failed to add {} shortcut. {}", ToWstring(shortcut), get_last_error_or_default(GetLastError()));
@@ -73,8 +76,18 @@ namespace CentralizedHotkeys
void UnregisterHotkeysForModule(std::wstring moduleName)
{
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
for (auto it = actions.begin(); it != actions.end(); it++)
{
for (auto action : it->second)
{
if (action.moduleName == moduleName)
{
HotkeyConflictDetector::Hotkey hotkey = HotkeyConflictDetector::ShortcutToHotkey(it->first);
hkmng.RemoveHotkey(hotkey, moduleName);
}
}
auto val = std::find_if(it->second.begin(), it->second.end(), [moduleName](Action a) { return a.moduleName == moduleName; });
if (val != it->second.end())
{

View File

@@ -20,11 +20,13 @@ namespace CentralizedHotkeys
{
WORD modifiersMask;
WORD vkCode;
const wchar_t* hotkeyName;
Shortcut(WORD modifiersMask = 0, WORD vkCode = 0)
Shortcut(WORD modifiersMask = 0, WORD vkCode = 0, const wchar_t* hotkeyName = nullptr)
{
this->modifiersMask = modifiersMask;
this->vkCode = vkCode;
this->hotkeyName = hotkeyName;
}
bool operator<(const Shortcut& key) const
@@ -35,7 +37,7 @@ namespace CentralizedHotkeys
std::wstring ToWstring(const Shortcut& shortcut);
bool AddHotkeyAction(Shortcut shortcut, Action action);
bool AddHotkeyAction(Shortcut shortcut, Action action, std::wstring moduleName, bool isEnabled);
void UnregisterHotkeysForModule(std::wstring moduleName);

View File

@@ -1,5 +1,6 @@
#include "pch.h"
#include "centralized_kb_hook.h"
#include "hotkey_conflict_detector.h"
#include <common/debug_control.h>
#include <common/utils/winapi_error.h>
#include <common/logger/logger.h>
@@ -187,10 +188,14 @@ namespace CentralizedKeyboardHook
return CallNextHookEx(hHook, nCode, wParam, lParam);
}
void SetHotkeyAction(const std::wstring& moduleName, const Hotkey& hotkey, std::function<bool()>&& action) noexcept
void SetHotkeyAction(const std::wstring& moduleName, const Hotkey& hotkey, bool isEnabled, std::function<bool()>&& action) noexcept
{
Logger::trace(L"Register hotkey action for {}", moduleName);
std::unique_lock lock{ mutex };
HotkeyConflictDetector::HotkeyConflictManager& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
hkmng.AddHotkey(hotkey, moduleName.c_str(), hotkey.name, isEnabled);
hotkeyDescriptors.insert({ .hotkey = hotkey, .moduleName = moduleName, .action = std::move(action) });
}
@@ -238,6 +243,10 @@ namespace CentralizedKeyboardHook
}
}
}
Logger::info(L"Removing all hotkeys of {}", moduleName);
HotkeyConflictDetector::HotkeyConflictManager& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
hkmng.RemoveHotkeyByModule(moduleName);
}
void Start() noexcept

View File

@@ -8,7 +8,7 @@ namespace CentralizedKeyboardHook
void Start() noexcept;
void Stop() noexcept;
void SetHotkeyAction(const std::wstring& moduleName, const Hotkey& hotkey, std::function<bool()>&& action) noexcept;
void SetHotkeyAction(const std::wstring& moduleName, const Hotkey& hotkey, bool isEnabled,std::function<bool()>&& action) noexcept;
void AddPressedKeyAction(const std::wstring& moduleName, const DWORD vk, const UINT milliseconds, std::function<bool()>&& action) noexcept;
void ClearModuleHotkeys(const std::wstring& moduleName) noexcept;
void RegisterWindow(HWND hwnd) noexcept;

View File

@@ -3,6 +3,7 @@
#include "auto_start_helper.h"
#include "tray_icon.h"
#include "Generated files/resource.h"
#include "hotkey_conflict_detector.h"
#include <common/SettingsAPI/settings_helpers.h>
#include "powertoy_module.h"
@@ -204,11 +205,15 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save)
{
Logger::info(L"apply_general_settings: Enabling powertoy {}", name);
powertoy->enable();
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
hkmng.EnableHotkeyByModule(name);
}
else
{
Logger::info(L"apply_general_settings: Disabling powertoy {}", name);
powertoy->disable();
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
hkmng.DisableHotkeyByModule(name);
}
// Sync the hotkey state with the module state, so it can be removed for disabled modules.
powertoy.UpdateHotkeyEx();
@@ -315,7 +320,9 @@ void start_enabled_powertoys()
{
Logger::info(L"start_enabled_powertoys: Enabling powertoy {}", name);
powertoy->enable();
powertoy.UpdateHotkeyEx();
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
hkmng.EnableHotkeyByModule(name);
// powertoy.UpdateHotkeyEx();
}
}
}

View File

@@ -0,0 +1,526 @@
#include "pch.h"
#include "hotkey_conflict_detector.h"
#include <common/SettingsAPI/settings_helpers.h>
#include <windows.h>
#include <unordered_map>
#include <cwchar>
namespace HotkeyConflictDetector
{
Hotkey ShortcutToHotkey(const CentralizedHotkeys::Shortcut& shortcut)
{
Hotkey hotkey;
hotkey.win = (shortcut.modifiersMask & MOD_WIN) != 0;
hotkey.ctrl = (shortcut.modifiersMask & MOD_CONTROL) != 0;
hotkey.shift = (shortcut.modifiersMask & MOD_SHIFT) != 0;
hotkey.alt = (shortcut.modifiersMask & MOD_ALT) != 0;
hotkey.key = shortcut.vkCode > 255 ? 0 : static_cast<unsigned char>(shortcut.vkCode);
return hotkey;
}
HotkeyConflictManager* HotkeyConflictManager::instance = nullptr;
std::mutex HotkeyConflictManager::instanceMutex;
HotkeyConflictManager& HotkeyConflictManager::GetInstance()
{
std::lock_guard<std::mutex> lock(instanceMutex);
if (instance == nullptr)
{
instance = new HotkeyConflictManager();
}
return *instance;
}
HotkeyConflictType HotkeyConflictManager::HasConflict(Hotkey const& _hotkey, const wchar_t* _moduleName, const wchar_t* _hotkeyName)
{
if (disabledHotkeys.find(_moduleName) != disabledHotkeys.end())
{
return HotkeyConflictType::NoConflict;
}
uint16_t handle = GetHotkeyHandle(_hotkey);
if (handle == 0)
{
return HotkeyConflictType::NoConflict;
}
// The order is important, first to check sys conflict and then inapp conflict
if (sysConflictHotkeyMap.find(handle) != sysConflictHotkeyMap.end())
{
return HotkeyConflictType::SystemConflict;
}
if (inAppConflictHotkeyMap.find(handle) != inAppConflictHotkeyMap.end())
{
return HotkeyConflictType::InAppConflict;
}
auto it = hotkeyMap.find(handle);
if (it == hotkeyMap.end())
{
return HasConflictWithSystemHotkey(_hotkey) ?
HotkeyConflictType::SystemConflict :
HotkeyConflictType::NoConflict;
}
if (wcscmp(it->second.moduleName.c_str(), _moduleName) == 0 && wcscmp(it->second.hotkeyName.c_str(), _hotkeyName) == 0)
{
// A shortcut matching its own assignment is not considered a conflict.
return HotkeyConflictType::NoConflict;
}
return HotkeyConflictType::InAppConflict;
}
// This function should only be called when a conflict has already been identified.
// It returns a list of all conflicting shortcuts.
std::vector<HotkeyConflictInfo> HotkeyConflictManager::GetAllConflicts(Hotkey const& _hotkey)
{
std::vector<HotkeyConflictInfo> conflicts;
uint16_t handle = GetHotkeyHandle(_hotkey);
// Check in-app conflicts first
auto inAppIt = inAppConflictHotkeyMap.find(handle);
if (inAppIt != inAppConflictHotkeyMap.end())
{
// Add all in-app conflicts
for (const auto& conflict : inAppIt->second)
{
conflicts.push_back(conflict);
}
return conflicts;
}
// Check system conflicts
auto sysIt = sysConflictHotkeyMap.find(handle);
if (sysIt != sysConflictHotkeyMap.end())
{
HotkeyConflictInfo systemConflict;
systemConflict.hotkey = _hotkey;
systemConflict.moduleName = L"System";
systemConflict.hotkeyName = L"System Hotkey";
conflicts.push_back(systemConflict);
return conflicts;
}
// Check if there's a successfully registered hotkey that would conflict
auto registeredIt = hotkeyMap.find(handle);
if (registeredIt != hotkeyMap.end())
{
conflicts.push_back(registeredIt->second);
return conflicts;
}
// If all the above conditions are ruled out, a system-level conflict is the only remaining explanation.
HotkeyConflictInfo systemConflict;
systemConflict.hotkey = _hotkey;
systemConflict.moduleName = L"System";
systemConflict.hotkeyName = L"System Hotkey";
conflicts.push_back(systemConflict);
return conflicts;
}
bool HotkeyConflictManager::AddHotkey(Hotkey const& _hotkey, const wchar_t* _moduleName, const wchar_t* _hotkeyName, bool isEnabled)
{
if (!isEnabled)
{
disabledHotkeys[_moduleName].push_back({ _hotkey, _moduleName, _hotkeyName });
return true;
}
uint16_t handle = GetHotkeyHandle(_hotkey);
if (handle == 0)
{
return false;
}
HotkeyConflictType conflictType = HasConflict(_hotkey, _moduleName, _hotkeyName );
if (conflictType != HotkeyConflictType::NoConflict)
{
if (conflictType == HotkeyConflictType::InAppConflict)
{
auto hotkeyFound = hotkeyMap.find(handle);
inAppConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyName });
if (hotkeyFound != hotkeyMap.end())
{
inAppConflictHotkeyMap[handle].insert(hotkeyFound->second);
hotkeyMap.erase(hotkeyFound);
}
}
else
{
sysConflictHotkeyMap[handle].insert({ _hotkey, _moduleName, _hotkeyName });
}
return false;
}
HotkeyConflictInfo hotkeyInfo;
hotkeyInfo.moduleName = _moduleName;
hotkeyInfo.hotkeyName = _hotkeyName;
hotkeyInfo.hotkey = _hotkey;
hotkeyMap[handle] = hotkeyInfo;
return true;
}
bool HotkeyConflictManager::RemoveHotkey(Hotkey const& _hotkey, const std::wstring& moduleName)
{
uint16_t handle = GetHotkeyHandle(_hotkey);
bool foundRecord = false;
if (disabledHotkeys.find(moduleName) != disabledHotkeys.end())
{
auto& hotkeys = disabledHotkeys[moduleName];
for (auto it = hotkeys.begin(); it != hotkeys.end();)
{
if (it->hotkey == _hotkey)
{
it = hotkeys.erase(it);
}
else
{
++it;
}
}
if (hotkeys.empty())
{
disabledHotkeys.erase(moduleName);
}
}
auto it = hotkeyMap.find(handle);
if (it != hotkeyMap.end() && it->second.moduleName == moduleName)
{
hotkeyMap.erase(it);
foundRecord = true;
}
auto it_sys = sysConflictHotkeyMap.find(handle);
if (it_sys != sysConflictHotkeyMap.end())
{
auto& sysConflicts = it_sys->second;
for (auto it_conf = sysConflicts.begin(); it_conf != sysConflicts.end();)
{
if (it_conf->moduleName == moduleName)
{
it_conf = sysConflicts.erase(it_conf);
foundRecord = true;
}
else
{
++it_conf;
}
}
if (sysConflicts.empty())
{
sysConflictHotkeyMap.erase(it_sys);
}
}
auto it_inApp = inAppConflictHotkeyMap.find(handle);
if (it_inApp != inAppConflictHotkeyMap.end())
{
auto& inAppConflicts = it_inApp->second;
for (auto it_conf = inAppConflicts.begin(); it_conf != inAppConflicts.end();)
{
if (it_conf->moduleName == moduleName)
{
it_conf = inAppConflicts.erase(it_conf);
foundRecord = true;
}
else
{
++it_conf;
}
}
if (inAppConflicts.size() == 1)
{
const auto& onlyConflict = *inAppConflicts.begin();
hotkeyMap[handle] = onlyConflict;
inAppConflictHotkeyMap.erase(it_inApp);
}
if (inAppConflicts.empty())
{
inAppConflictHotkeyMap.erase(it_inApp);
}
}
return foundRecord;
}
std::vector<HotkeyConflictInfo> HotkeyConflictManager::RemoveHotkeyByModule(const std::wstring& moduleName)
{
std::vector<HotkeyConflictInfo> removedHotkeys;
if (disabledHotkeys.find(moduleName) != disabledHotkeys.end())
{
disabledHotkeys.erase(moduleName);
}
std::lock_guard<std::mutex> lock(hotkeyMutex);
bool foundRecord = false;
for (auto it = sysConflictHotkeyMap.begin(); it != sysConflictHotkeyMap.end();)
{
auto& conflictSet = it->second;
for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();)
{
if (setIt->moduleName == moduleName)
{
removedHotkeys.push_back(*setIt);
setIt = conflictSet.erase(setIt);
foundRecord = true;
}
else
{
++setIt;
}
}
if (conflictSet.empty())
{
it = sysConflictHotkeyMap.erase(it);
}
else
{
++it;
}
}
for (auto it = inAppConflictHotkeyMap.begin(); it != inAppConflictHotkeyMap.end();)
{
auto& conflictSet = it->second;
uint16_t handle = it->first;
for (auto setIt = conflictSet.begin(); setIt != conflictSet.end();)
{
if (setIt->moduleName == moduleName)
{
removedHotkeys.push_back(*setIt);
setIt = conflictSet.erase(setIt);
foundRecord = true;
}
else
{
++setIt;
}
}
if (conflictSet.empty())
{
it = inAppConflictHotkeyMap.erase(it);
}
else if (conflictSet.size() == 1)
{
// Move the only remaining conflict to main map
const auto& onlyConflict = *conflictSet.begin();
hotkeyMap[handle] = onlyConflict;
it = inAppConflictHotkeyMap.erase(it);
}
else
{
++it;
}
}
for (auto it = hotkeyMap.begin(); it != hotkeyMap.end();)
{
if (it->second.moduleName == moduleName)
{
uint16_t handle = it->first;
removedHotkeys.push_back(it->second);
it = hotkeyMap.erase(it);
foundRecord = true;
auto inAppIt = inAppConflictHotkeyMap.find(handle);
if (inAppIt != inAppConflictHotkeyMap.end() && inAppIt->second.size() == 1)
{
// Move the only in-app conflict to main map
const auto& onlyConflict = *inAppIt->second.begin();
hotkeyMap[handle] = onlyConflict;
inAppConflictHotkeyMap.erase(inAppIt);
}
}
else
{
++it;
}
}
return removedHotkeys;
}
void HotkeyConflictManager::EnableHotkeyByModule(const std::wstring& moduleName)
{
if (disabledHotkeys.find(moduleName) == disabledHotkeys.end())
{
return; // No disabled hotkeys for this module
}
auto hotkeys = disabledHotkeys[moduleName];
disabledHotkeys.erase(moduleName);
for (const auto& hotkeyInfo : hotkeys)
{
// Re-add the hotkey as enabled
AddHotkey(hotkeyInfo.hotkey, moduleName.c_str(), hotkeyInfo.hotkeyName.c_str(), true);
}
}
void HotkeyConflictManager::DisableHotkeyByModule(const std::wstring& moduleName)
{
auto hotkeys = RemoveHotkeyByModule(moduleName);
disabledHotkeys[moduleName] = hotkeys;
}
bool HotkeyConflictManager::HasConflictWithSystemHotkey(const Hotkey& hotkey)
{
// Convert PowerToys Hotkey format to Win32 RegisterHotKey format
UINT modifiers = 0;
if (hotkey.win)
{
modifiers |= MOD_WIN;
}
if (hotkey.ctrl)
{
modifiers |= MOD_CONTROL;
}
if (hotkey.alt)
{
modifiers |= MOD_ALT;
}
if (hotkey.shift)
{
modifiers |= MOD_SHIFT;
}
// No modifiers or no key is not a valid hotkey
if (modifiers == 0 || hotkey.key == 0)
{
return false;
}
// Use a unique ID for this test registration
const int hotkeyId = 0x0FFF; // Arbitrary ID for temporary registration
// Try to register the hotkey with Windows, using nullptr instead of a window handle
if (!RegisterHotKey(nullptr, hotkeyId, modifiers, hotkey.key))
{
// If registration fails with ERROR_HOTKEY_ALREADY_REGISTERED, it means the hotkey
// is already in use by the system or another application
if (GetLastError() == ERROR_HOTKEY_ALREADY_REGISTERED)
{
return true;
}
}
else
{
// If registration succeeds, unregister it immediately
UnregisterHotKey(nullptr, hotkeyId);
}
return false;
}
json::JsonObject HotkeyConflictManager::GetHotkeyConflictsAsJson()
{
std::lock_guard<std::mutex> lock(hotkeyMutex);
using namespace json;
JsonObject root;
// Serialize hotkey to a unique string format for grouping
auto serializeHotkey = [](const Hotkey& hotkey) -> JsonObject {
JsonObject obj;
obj.Insert(L"win", value(hotkey.win));
obj.Insert(L"ctrl", value(hotkey.ctrl));
obj.Insert(L"shift", value(hotkey.shift));
obj.Insert(L"alt", value(hotkey.alt));
obj.Insert(L"key", value(static_cast<int>(hotkey.key)));
return obj;
};
// New format: Group conflicts by hotkey
JsonArray inAppConflictsArray;
JsonArray sysConflictsArray;
// Process in-app conflicts - only include hotkeys that are actually in conflict
for (const auto& [handle, conflicts] : inAppConflictHotkeyMap)
{
if (!conflicts.empty())
{
JsonObject conflictGroup;
// All entries have the same hotkey, so use the first one for the key
conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey));
// Create an array of module info without repeating the hotkey
JsonArray modules;
for (const auto& info : conflicts)
{
JsonObject moduleInfo;
moduleInfo.Insert(L"moduleName", value(info.moduleName));
moduleInfo.Insert(L"hotkeyName", value(info.hotkeyName));
modules.Append(moduleInfo);
}
conflictGroup.Insert(L"modules", modules);
inAppConflictsArray.Append(conflictGroup);
}
}
// Process system conflicts - only include hotkeys that are actually in conflict
for (const auto& [handle, conflicts] : sysConflictHotkeyMap)
{
if (!conflicts.empty())
{
JsonObject conflictGroup;
// All entries have the same hotkey, so use the first one for the key
conflictGroup.Insert(L"hotkey", serializeHotkey(conflicts.begin()->hotkey));
// Create an array of module info without repeating the hotkey
JsonArray modules;
for (const auto& info : conflicts)
{
JsonObject moduleInfo;
moduleInfo.Insert(L"moduleName", value(info.moduleName));
moduleInfo.Insert(L"hotkeyName", value(info.hotkeyName));
modules.Append(moduleInfo);
}
conflictGroup.Insert(L"modules", modules);
sysConflictsArray.Append(conflictGroup);
}
}
// Add the grouped conflicts to the root object
root.Insert(L"inAppConflicts", inAppConflictsArray);
root.Insert(L"sysConflicts", sysConflictsArray);
return root;
}
uint16_t HotkeyConflictManager::GetHotkeyHandle(const Hotkey& hotkey)
{
uint16_t handle = hotkey.key;
handle |= hotkey.win << 8;
handle |= hotkey.ctrl << 9;
handle |= hotkey.shift << 10;
handle |= hotkey.alt << 11;
return handle;
}
}

View File

@@ -0,0 +1,103 @@
#pragma once
#include "pch.h"
#include <unordered_map>
#include <unordered_set>
#include <string>
#include "../modules/interface/powertoy_module_interface.h"
#include "centralized_hotkeys.h"
#include "common/utils/json.h"
namespace HotkeyConflictDetector
{
using Hotkey = PowertoyModuleIface::Hotkey;
using HotkeyEx = PowertoyModuleIface::HotkeyEx;
using Shortcut = CentralizedHotkeys::Shortcut;
struct HotkeyConflictInfo
{
Hotkey hotkey;
std::wstring moduleName;
std::wstring hotkeyName;
bool usingKBHook = true;
inline bool operator==(const HotkeyConflictInfo& other) const
{
return hotkey == other.hotkey &&
moduleName == other.moduleName &&
hotkeyName == other.hotkeyName;
}
};
Hotkey ShortcutToHotkey(const CentralizedHotkeys::Shortcut& shortcut);
enum HotkeyConflictType
{
NoConflict = 0,
SystemConflict = 1,
InAppConflict = 2,
};
class HotkeyConflictManager
{
public:
static HotkeyConflictManager& GetInstance();
HotkeyConflictType HasConflict(const Hotkey& hotkey, const wchar_t* moduleName, const wchar_t* hotkeyName);
std::vector<HotkeyConflictInfo> HotkeyConflictManager::GetAllConflicts(Hotkey const& _hotkey);
bool AddHotkey(const Hotkey& hotkey, const wchar_t* moduleName, const wchar_t* hotkeyName, bool isEnabled);
bool RemoveHotkey(const Hotkey& hotkey, const std::wstring& moduleName);
std::vector<HotkeyConflictInfo> RemoveHotkeyByModule(const std::wstring& moduleName);
void EnableHotkeyByModule(const std::wstring& moduleName);
void DisableHotkeyByModule(const std::wstring& moduleName);
json::JsonObject GetHotkeyConflictsAsJson();
private:
static std::mutex instanceMutex;
static HotkeyConflictManager* instance;
std::mutex hotkeyMutex;
// Hotkey in hotkeyMap means the hotkey has been registered successfully
std::unordered_map<uint16_t, HotkeyConflictInfo> hotkeyMap;
// Hotkey in sysConflictHotkeyMap means the hotkey has conflict with system defined hotkeys
std::unordered_map<uint16_t, std::unordered_set<HotkeyConflictInfo>> sysConflictHotkeyMap;
// Hotkey in inAppConflictHotkeyMap means the hotkey has conflict with other modules
std::unordered_map<uint16_t, std::unordered_set<HotkeyConflictInfo>> inAppConflictHotkeyMap;
std::unordered_map<std::wstring, std::vector<HotkeyConflictInfo>> disabledHotkeys;
uint16_t GetHotkeyHandle(const Hotkey&);
bool HasConflictWithSystemHotkey(const Hotkey&);
HotkeyConflictManager() = default;
};
};
namespace std
{
template<>
struct hash<HotkeyConflictDetector::HotkeyConflictInfo>
{
size_t operator()(const HotkeyConflictDetector::HotkeyConflictInfo& info) const
{
size_t hotkeyHash =
(info.hotkey.win ? 1ULL : 0ULL) |
((info.hotkey.ctrl ? 1ULL : 0ULL) << 1) |
((info.hotkey.shift ? 1ULL : 0ULL) << 2) |
((info.hotkey.alt ? 1ULL : 0ULL) << 3) |
(static_cast<size_t>(info.hotkey.key) << 4);
size_t moduleHash = std::hash<std::wstring>{}(info.moduleName);
size_t nameHash = std::hash<std::wstring>{}(info.hotkeyName);
return hotkeyHash ^
((moduleHash << 1) | (moduleHash >> (sizeof(size_t) * 8 - 1))) ^ // rotate left 1 bit
((nameHash << 2) | (nameHash >> (sizeof(size_t) * 8 - 2))); // rotate left 2 bits
}
};
}

View File

@@ -55,6 +55,15 @@ void PowertoyModule::update_hotkeys()
{
CentralizedKeyboardHook::ClearModuleHotkeys(pt_module->get_key());
if (pt_module->is_enabled())
{
Logger::trace(L"{} module is enabled, registering hotkeys", pt_module->get_key());
}
else
{
Logger::trace(L"{} module is disabled, unregistering hotkeys", pt_module->get_key());
}
size_t hotkeyCount = pt_module->get_hotkeys(nullptr, 0);
std::vector<PowertoyModuleIface::Hotkey> hotkeys(hotkeyCount);
pt_module->get_hotkeys(hotkeys.data(), hotkeyCount);
@@ -63,7 +72,7 @@ void PowertoyModule::update_hotkeys()
for (size_t i = 0; i < hotkeyCount; i++)
{
CentralizedKeyboardHook::SetHotkeyAction(pt_module->get_key(), hotkeys[i], [modulePtr, i] {
CentralizedKeyboardHook::SetHotkeyAction(pt_module->get_key(), hotkeys[i], pt_module->is_enabled(), [modulePtr, i] {
Logger::trace(L"{} hotkey is invoked from Centralized keyboard hook", modulePtr->get_key());
return modulePtr->on_hotkey(i);
});
@@ -83,7 +92,7 @@ void PowertoyModule::UpdateHotkeyEx()
modulePtr->OnHotkeyEx();
};
CentralizedHotkeys::AddHotkeyAction({ hotkey.modifiersMask, hotkey.vkCode }, { pt_module->get_key(), action });
CentralizedHotkeys::AddHotkeyAction({ hotkey.modifiersMask, hotkey.vkCode, hotkey.name }, { pt_module->get_key(), action }, pt_module->get_name(), pt_module->is_enabled());
}
// HACK:

View File

@@ -51,6 +51,7 @@
<ClCompile Include="bug_report.cpp" />
<ClCompile Include="centralized_hotkeys.cpp" />
<ClCompile Include="general_settings.cpp" />
<ClCompile Include="hotkey_conflict_detector.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
@@ -71,6 +72,7 @@
<ClInclude Include="bug_report.h" />
<ClInclude Include="centralized_hotkeys.h" />
<ClInclude Include="general_settings.h" />
<ClInclude Include="hotkey_conflict_detector.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="centralized_kb_hook.h" />
<ClInclude Include="settings_telemetry.h" />

View File

@@ -45,6 +45,9 @@
<ClCompile Include="bug_report.cpp">
<Filter>Utils</Filter>
</ClCompile>
<ClCompile Include="hotkey_conflict_detector.cpp">
<Filter>Utils</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
@@ -93,6 +96,9 @@
<ClInclude Include="bug_report.h">
<Filter>Utils</Filter>
</ClInclude>
<ClInclude Include="hotkey_conflict_detector.h">
<Filter>Utils</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Utils">

View File

@@ -13,6 +13,7 @@
#include "UpdateUtils.h"
#include "centralized_kb_hook.h"
#include "Generated files/resource.h"
#include "hotkey_conflict_detector.h"
#include <common/utils/json.h>
#include <common/SettingsAPI/settings_helpers.cpp>
@@ -249,6 +250,79 @@ void dispatch_received_json(const std::wstring& json_to_parse)
const std::wstring save_file_location = PTSettingsHelper::get_root_save_folder_location() + language_filename;
json::to_file(save_file_location, j);
}
else if (name == L"check_hotkey_conflict")
{
try
{
PowertoyModuleIface::Hotkey hotkey;
hotkey.win = value.GetObjectW().GetNamedBoolean(L"win", false);
hotkey.ctrl = value.GetObjectW().GetNamedBoolean(L"ctrl", false);
hotkey.shift = value.GetObjectW().GetNamedBoolean(L"shift", false);
hotkey.alt = value.GetObjectW().GetNamedBoolean(L"alt", false);
hotkey.key = static_cast<unsigned char>(value.GetObjectW().GetNamedNumber(L"key", 0));
std::wstring requestId = value.GetObjectW().GetNamedString(L"request_id", L"").c_str();
std::wstring moduleName = value.GetObjectW().GetNamedString(L"moduleName", L"").c_str();
std::wstring hotkeyName = value.GetObjectW().GetNamedString(L"hotkeyName", L"").c_str();
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
bool hasConflict = hkmng.HasConflict(hotkey, moduleName.c_str(), hotkeyName.c_str());
json::JsonObject response;
response.SetNamedValue(L"response_type", json::JsonValue::CreateStringValue(L"hotkey_conflict_result"));
response.SetNamedValue(L"request_id", json::JsonValue::CreateStringValue(requestId));
response.SetNamedValue(L"has_conflict", json::JsonValue::CreateBooleanValue(hasConflict));
if (hasConflict)
{
auto conflicts = hkmng.GetAllConflicts(hotkey);
if (!conflicts.empty())
{
// Include all conflicts in the response
json::JsonArray allConflicts;
for (const auto& conflict : conflicts)
{
json::JsonObject conflictObj;
conflictObj.SetNamedValue(L"module", json::JsonValue::CreateStringValue(conflict.moduleName));
conflictObj.SetNamedValue(L"hotkey_name", json::JsonValue::CreateStringValue(conflict.hotkeyName));
allConflicts.Append(conflictObj);
}
response.SetNamedValue(L"all_conflicts", allConflicts);
}
}
std::unique_lock lock{ ipc_mutex };
if (current_settings_ipc)
{
current_settings_ipc->send(response.Stringify().c_str());
}
}
catch (...)
{
Logger::error(L"Failed to process hotkey conflict check request");
}
}
else if (name == L"get_all_hotkey_conflicts")
{
try
{
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
auto conflictsJson = hkmng.GetHotkeyConflictsAsJson();
// Add response type identifier
conflictsJson.SetNamedValue(L"response_type", json::JsonValue::CreateStringValue(L"all_hotkey_conflicts"));
std::unique_lock lock{ ipc_mutex };
if (current_settings_ipc)
{
current_settings_ipc->send(conflictsJson.Stringify().c_str());
}
}
catch (...)
{
Logger::error(L"Failed to process get all hotkey conflicts request");
}
}
}
return;
}
@@ -671,14 +745,22 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
{
switch (value)
{
case ESettingsWindowNames::Dashboard:
return "Dashboard";
case ESettingsWindowNames::Overview:
return "Overview";
case ESettingsWindowNames::AlwaysOnTop:
return "AlwaysOnTop";
case ESettingsWindowNames::Awake:
return "Awake";
case ESettingsWindowNames::ColorPicker:
return "ColorPicker";
case ESettingsWindowNames::CmdNotFound:
return "CmdNotFound";
case ESettingsWindowNames::FancyZones:
return "FancyZones";
case ESettingsWindowNames::FileLocksmith:
return "FileLocksmith";
case ESettingsWindowNames::Run:
return "Run";
case ESettingsWindowNames::ImageResizer:
@@ -687,6 +769,16 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
return "KBM";
case ESettingsWindowNames::MouseUtils:
return "MouseUtils";
case ESettingsWindowNames::MouseWithoutBorders:
return "MouseWithoutBorders";
case ESettingsWindowNames::Peek:
return "Peek";
case ESettingsWindowNames::PowerAccent:
return "PowerAccent";
case ESettingsWindowNames::PowerLauncher:
return "PowerLauncher";
case ESettingsWindowNames::PowerPreview:
return "PowerPreview";
case ESettingsWindowNames::PowerRename:
return "PowerRename";
case ESettingsWindowNames::FileExplorer:
@@ -707,8 +799,6 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
return "CropAndLock";
case ESettingsWindowNames::EnvironmentVariables:
return "EnvironmentVariables";
case ESettingsWindowNames::Dashboard:
return "Dashboard";
case ESettingsWindowNames::AdvancedPaste:
return "AdvancedPaste";
case ESettingsWindowNames::NewPlus:
@@ -728,10 +818,18 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
{
if (value == "Overview")
if (value == "Dashboard")
{
return ESettingsWindowNames::Dashboard;
}
else if (value == "Overview")
{
return ESettingsWindowNames::Overview;
}
else if (value == "AlwaysOnTop")
{
return ESettingsWindowNames::AlwaysOnTop;
}
else if (value == "Awake")
{
return ESettingsWindowNames::Awake;
@@ -740,10 +838,18 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
{
return ESettingsWindowNames::ColorPicker;
}
else if (value == "CmdNotFound")
{
return ESettingsWindowNames::CmdNotFound;
}
else if (value == "FancyZones")
{
return ESettingsWindowNames::FancyZones;
}
else if (value == "FileLocksmith")
{
return ESettingsWindowNames::FileLocksmith;
}
else if (value == "Run")
{
return ESettingsWindowNames::Run;
@@ -760,6 +866,26 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
{
return ESettingsWindowNames::MouseUtils;
}
else if (value == "MouseWithoutBorders")
{
return ESettingsWindowNames::MouseWithoutBorders;
}
else if (value == "Peek")
{
return ESettingsWindowNames::Peek;
}
else if (value == "PowerAccent")
{
return ESettingsWindowNames::PowerAccent;
}
else if (value == "PowerLauncher")
{
return ESettingsWindowNames::PowerLauncher;
}
else if (value == "PowerPreview")
{
return ESettingsWindowNames::PowerPreview;
}
else if (value == "PowerRename")
{
return ESettingsWindowNames::PowerRename;
@@ -800,10 +926,6 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
{
return ESettingsWindowNames::EnvironmentVariables;
}
else if (value == "Dashboard")
{
return ESettingsWindowNames::Dashboard;
}
else if (value == "AdvancedPaste")
{
return ESettingsWindowNames::AdvancedPaste;

View File

@@ -6,13 +6,21 @@ enum class ESettingsWindowNames
{
Dashboard = 0,
Overview,
AlwaysOnTop,
Awake,
ColorPicker,
CmdNotFound,
FancyZones,
FileLocksmith,
Run,
ImageResizer,
KBM,
MouseUtils,
MouseWithoutBorders,
Peek,
PowerAccent,
PowerLauncher,
PowerPreview,
PowerRename,
FileExplorer,
ShortcutGuide,

View File

@@ -12,7 +12,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvancedPasteAction
{
private HotkeySettings _shortcut = new();
private bool _isShown = true;
private bool _isShown;
private bool _hasConflict;
private string _tooltip;
[JsonPropertyName("shortcut")]
public HotkeySettings Shortcut
@@ -38,6 +40,20 @@ public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
set => Set(ref _isShown, value);
}
[JsonIgnore]
public bool HasConflict
{
get => _hasConflict;
set => Set(ref _hasConflict, value);
}
[JsonIgnore]
public string Tooltip
{
get => _tooltip;
set => Set(ref _tooltip, value);
}
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> SubActions => [];
}

View File

@@ -5,8 +5,8 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
namespace Microsoft.PowerToys.Settings.UI.Library;
@@ -20,6 +20,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private bool _canMoveUp;
private bool _canMoveDown;
private bool _isValid;
private bool _hasConflict;
private string _tooltip;
[JsonPropertyName("id")]
public int Id
@@ -65,7 +67,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
// We null-coalesce here rather than outside this branch as we want to raise PropertyChanged when the setter is called
// with null; the ShortcutControl depends on this.
_shortcut = value ?? new();
_shortcut.HotkeyName = $"CustomAction_{this.Id}";
_shortcut.OwnerModuleName = AdvancedPasteSettings.ModuleName;
OnPropertyChanged();
}
}
@@ -99,6 +102,20 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private set => Set(ref _isValid, value);
}
[JsonIgnore]
public bool HasConflict
{
get => _hasConflict;
set => Set(ref _hasConflict, value);
}
[JsonIgnore]
public string Tooltip
{
get => _tooltip;
set => Set(ref _tooltip, value);
}
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> SubActions => [];
@@ -118,6 +135,8 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
IsShown = other.IsShown;
CanMoveUp = other.CanMoveUp;
CanMoveDown = other.CanMoveDown;
HasConflict = other.HasConflict;
Tooltip = other.Tooltip;
}
private HotkeySettings GetShortcutClone()
@@ -128,7 +147,10 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
_ = HotkeySettings.TryParseFromCmd(shortcutString, out shortcut);
}
return (shortcut as HotkeySettings) ?? new HotkeySettings();
var result = (shortcut as HotkeySettings) ?? new HotkeySettings();
result.HotkeyName = $"CustomAction_{this.Id}";
result.OwnerModuleName = AdvancedPasteSettings.ModuleName;
return result;
}
private void UpdateIsValid()

View File

@@ -11,16 +11,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
public class AdvancedPasteProperties
{
public static readonly HotkeySettings DefaultAdvancedPasteUIShortcut = new HotkeySettings(true, false, false, true, 0x56); // Win+Shift+V
public static readonly HotkeySettings DefaultAdvancedPasteUIShortcut = new HotkeySettings(true, false, false, true, 0x56, "AdvancedPasteUIShortcut", AdvancedPasteSettings.ModuleName); // Win+Shift+V
public static readonly HotkeySettings DefaultPasteAsPlainTextShortcut = new HotkeySettings(true, true, true, false, 0x56); // Ctrl+Win+Alt+V
public static readonly HotkeySettings DefaultPasteAsPlainTextShortcut = new HotkeySettings(true, true, true, false, 0x56, "PasteAsPlainTextShortcut", AdvancedPasteSettings.ModuleName); // Ctrl+Win+Alt+V
public AdvancedPasteProperties()
{
AdvancedPasteUIShortcut = DefaultAdvancedPasteUIShortcut;
PasteAsPlainTextShortcut = DefaultPasteAsPlainTextShortcut;
PasteAsMarkdownShortcut = new();
PasteAsJsonShortcut = new();
PasteAsMarkdownShortcut = new("PasteAsMarkdownShortcut", AdvancedPasteSettings.ModuleName);
PasteAsJsonShortcut = new("PasteAsJsonShortcut", AdvancedPasteSettings.ModuleName);
CustomActions = new();
AdditionalActions = new();
IsAdvancedAIEnabled = false;

View File

@@ -10,7 +10,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
// Needs to be kept in sync with src\modules\alwaysontop\AlwaysOnTop\Settings.h
public class AlwaysOnTopProperties
{
public static readonly HotkeySettings DefaultHotkeyValue = new HotkeySettings(true, true, false, false, 0x54);
public static readonly HotkeySettings DefaultHotkeyValue = new HotkeySettings(true, true, false, false, 0x54, "Hotkey", AlwaysOnTopSettings.ModuleName);
public const bool DefaultFrameEnabled = true;
public const int DefaultFrameThickness = 15;
public const string DefaultFrameColor = "#0099cc";

View File

@@ -11,7 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class CmdPalProperties
{
// Default shortcut - Win + Alt + Space
public static readonly HotkeySettings DefaultHotkeyValue = new HotkeySettings(true, false, true, false, 32);
public static readonly HotkeySettings DefaultHotkeyValue = new HotkeySettings(true, false, true, false, 32, "Hotkey", "CmdPal");
#pragma warning disable SA1401 // Fields should be private
#pragma warning disable CA1051 // Do not declare visible instance fields
@@ -44,6 +44,17 @@ namespace Microsoft.PowerToys.Settings.UI.Library
if (doc.RootElement.TryGetProperty(nameof(Hotkey), out JsonElement hotkeyElement))
{
Hotkey = JsonSerializer.Deserialize<HotkeySettings>(hotkeyElement.GetRawText());
if (Hotkey == null)
{
Hotkey = DefaultHotkeyValue;
}
if (Hotkey.HotkeyName == string.Empty)
{
Hotkey.HotkeyName = DefaultHotkeyValue.HotkeyName;
Hotkey.OwnerModuleName = DefaultHotkeyValue.OwnerModuleName;
}
}
}
catch (Exception)

View File

@@ -15,7 +15,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class ColorPickerProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x43);
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x43, "ActivationShortcut", ColorPickerSettings.ModuleName);
public ColorPickerProperties()
{

View File

@@ -12,7 +12,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
public class ColorPickerPropertiesVersion1
{
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x43);
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x43, "ActivationShortcut", ColorPickerSettings.ModuleName);
public ColorPickerPropertiesVersion1()
{

View File

@@ -9,8 +9,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
public class CropAndLockProperties
{
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 DefaultReparentHotkeyValue = new HotkeySettings(true, true, false, true, 0x52, "ReparentHotkey", CropAndLockSettings.ModuleName); // Ctrl+Win+Shift+R
public static readonly HotkeySettings DefaultThumbnailHotkeyValue = new HotkeySettings(true, true, false, true, 0x54, "ThumbnailHotkey", CropAndLockSettings.ModuleName); // Ctrl+Win+Shift+T
public CropAndLockProperties()
{

View File

@@ -16,9 +16,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public const int VkNext = 0x22;
public const int VkPrior = 0x21;
public static readonly HotkeySettings DefaultEditorHotkeyValue = new HotkeySettings(true, false, false, true, VkOem3);
public static readonly HotkeySettings DefaultNextTabHotkeyValue = new HotkeySettings(true, false, false, false, VkNext);
public static readonly HotkeySettings DefaultPrevTabHotkeyValue = new HotkeySettings(true, false, false, false, VkPrior);
public static readonly HotkeySettings DefaultEditorHotkeyValue = new HotkeySettings(true, false, false, true, VkOem3, "EditorHotkey", FancyZonesSettings.ModuleName);
public static readonly HotkeySettings DefaultNextTabHotkeyValue = new HotkeySettings(true, false, false, false, VkNext, "NextTabHotkey", FancyZonesSettings.ModuleName);
public static readonly HotkeySettings DefaultPrevTabHotkeyValue = new HotkeySettings(true, false, false, false, VkPrior, "PrevTabHotkey", FancyZonesSettings.ModuleName);
public FZConfigProperties()
{

View File

@@ -11,7 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class FindMyMouseProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x46);
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x46, "ActivationShortcut", FindMyMouseSettings.ModuleName);
[JsonPropertyName("activation_method")]
public IntProperty ActivationMethod { get; set; }

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class HotkeyConflictGroup
{
public HotkeySettings Hotkey { get; set; } = new HotkeySettings();
public List<ModuleHotkeyInfo> Modules { get; set; } = new List<ModuleHotkeyInfo>();
public bool IsSystemConflict { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class AllHotkeyConflictsData
{
public List<HotkeyConflictGroupData> InAppConflicts { get; set; } = new List<HotkeyConflictGroupData>();
public List<HotkeyConflictGroupData> SystemConflicts { get; set; } = new List<HotkeyConflictGroupData>();
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class AllHotkeyConflictsEventArgs : EventArgs
{
public AllHotkeyConflictsData Conflicts { get; }
public AllHotkeyConflictsEventArgs(AllHotkeyConflictsData conflicts)
{
Conflicts = conflicts;
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class HotkeyConflictGroupData
{
public HotkeyData Hotkey { get; set; }
public bool IsSystemConflict { get; set; }
public List<ModuleHotkeyData> Modules { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class HotkeyConflictInfo
{
public bool IsSystemConflict { get; set; }
public string ConflictingModuleName { get; set; }
public string ConflictingHotkeyName { get; set; }
public List<string> AllConflictingModules { get; set; } = new List<string>();
}
}

View File

@@ -0,0 +1,150 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class HotkeyData
{
public bool Win { get; set; }
public bool Ctrl { get; set; }
public bool Shift { get; set; }
public bool Alt { get; set; }
public int Key { get; set; }
public List<object> GetKeysList()
{
List<object> shortcutList = new List<object>();
if (Win)
{
shortcutList.Add(92); // The Windows key or button.
}
if (Ctrl)
{
shortcutList.Add("Ctrl");
}
if (Alt)
{
shortcutList.Add("Alt");
}
if (Shift)
{
shortcutList.Add(16); // The Shift key or button.
}
if (Key > 0)
{
switch (Key)
{
// https://learn.microsoft.com/uwp/api/windows.system.virtualkey?view=winrt-20348
case 38: // The Up Arrow key or button.
case 40: // The Down Arrow key or button.
case 37: // The Left Arrow key or button.
case 39: // The Right Arrow key or button.
shortcutList.Add(Key);
break;
default:
var localKey = Helper.GetKeyName((uint)Key);
shortcutList.Add(localKey);
break;
}
}
return shortcutList;
}
public override string ToString()
{
var output = new StringBuilder();
if (Win)
{
output.Append("Win + ");
}
if (Ctrl)
{
output.Append("Ctrl + ");
}
if (Alt)
{
output.Append("Alt + ");
}
if (Shift)
{
output.Append("Shift + ");
}
if (Key > 0)
{
// For virtual key codes, we can display the key name
// This follows the same pattern as HotkeySettings
var keyName = Helper.GetKeyName((uint)Key);
output.Append(keyName);
}
else if (output.Length >= 2)
{
// Remove the trailing " + " if there's no key
output.Remove(output.Length - 2, 2);
}
return output.ToString();
}
private static string GetKeyName(uint keyCode)
{
// Simple mapping for common virtual key codes
// This could be extended to use the Helper.GetKeyName method if available
return keyCode switch
{
0x08 => "Backspace",
0x09 => "Tab",
0x0D => "Enter",
0x1B => "Escape",
0x20 => "Space",
0x21 => "Page Up",
0x22 => "Page Down",
0x23 => "End",
0x24 => "Home",
0x25 => "Left",
0x26 => "Up",
0x27 => "Right",
0x28 => "Down",
0x2D => "Insert",
0x2E => "Delete",
>= 0x30 and <= 0x39 => ((char)keyCode).ToString(), // 0-9
>= 0x41 and <= 0x5A => ((char)keyCode).ToString(), // A-Z
0x70 => "F1",
0x71 => "F2",
0x72 => "F3",
0x73 => "F4",
0x74 => "F5",
0x75 => "F6",
0x76 => "F7",
0x77 => "F8",
0x78 => "F9",
0x79 => "F10",
0x7A => "F11",
0x7B => "F12",
_ => $"Key{keyCode}",
};
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class ModuleConflictsData
{
public List<HotkeyConflictGroupData> InAppConflicts { get; set; } = new List<HotkeyConflictGroupData>();
public List<HotkeyConflictGroupData> SystemConflicts { get; set; } = new List<HotkeyConflictGroupData>();
public bool HasConflicts => InAppConflicts.Count > 0 || SystemConflicts.Count > 0;
}
}

View File

@@ -0,0 +1,73 @@
// 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.ComponentModel;
using System.Runtime.CompilerServices;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
public class ModuleHotkeyData : INotifyPropertyChanged
{
private string _moduleName;
private string _hotkeyName;
private HotkeySettings _hotkeySettings;
private bool _isSystemConflict;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string ModuleName
{
get => _moduleName;
set
{
if (_moduleName != value)
{
_moduleName = value;
}
}
}
public string HotkeyName
{
get => _hotkeyName;
set
{
if (_hotkeyName != value)
{
_hotkeyName = value;
}
}
}
public HotkeySettings HotkeySettings
{
get => _hotkeySettings;
set
{
if (_hotkeySettings != value)
{
_hotkeySettings = value;
OnPropertyChanged();
}
}
}
public bool IsSystemConflict
{
get => _isSystemConflict;
set
{
if (_isSystemConflict != value)
{
_isSystemConflict = value;
}
}
}
}
}

View File

@@ -4,17 +4,29 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public record HotkeySettings : ICmdLineRepresentable
public record HotkeySettings : ICmdLineRepresentable, INotifyPropertyChanged
{
private const int VKTAB = 0x09;
private bool _hasConflict;
private string _conflictDescription;
private bool _isSystemConflict;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public HotkeySettings()
{
@@ -23,6 +35,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Alt = false;
Shift = false;
Code = 0;
HasConflict = false;
HotkeyName = string.Empty;
OwnerModuleName = string.Empty;
}
public HotkeySettings(string hotkeyName, string ownerModuleName)
{
Win = false;
Ctrl = false;
Alt = false;
Shift = false;
Code = 0;
HasConflict = false;
HotkeyName = hotkeyName;
OwnerModuleName = ownerModuleName;
}
/// <summary>
@@ -33,13 +62,60 @@ namespace Microsoft.PowerToys.Settings.UI.Library
/// <param name="alt">Should Alt key be used</param>
/// <param name="shift">Should Shift key be used</param>
/// <param name="code">Go to https://learn.microsoft.com/windows/win32/inputdev/virtual-key-codes to see list of v-keys</param>
public HotkeySettings(bool win, bool ctrl, bool alt, bool shift, int code)
public HotkeySettings(bool win, bool ctrl, bool alt, bool shift, int code, string hotkeyName = "", string ownerModuleName = "", bool hasConflict = false)
{
Win = win;
Ctrl = ctrl;
Alt = alt;
Shift = shift;
Code = code;
HasConflict = hasConflict;
HotkeyName = hotkeyName;
OwnerModuleName = ownerModuleName;
}
public bool HasConflict
{
get => _hasConflict;
set
{
if (_hasConflict != value)
{
_hasConflict = value;
OnPropertyChanged();
}
}
}
public string ConflictDescription
{
get => _conflictDescription ?? string.Empty;
set
{
if (_conflictDescription != value)
{
_conflictDescription = value;
OnPropertyChanged();
}
}
}
public bool IsSystemConflict
{
get => _isSystemConflict;
set
{
if (_isSystemConflict != value)
{
_isSystemConflict = value;
OnPropertyChanged();
}
}
}
public virtual void UpdateConflictStatus()
{
Logger.LogInfo($"{this.ToString()}");
}
[JsonPropertyName("win")]
@@ -57,6 +133,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("code")]
public int Code { get; set; }
[JsonPropertyName("hotkeyName")]
public string HotkeyName { get; set; }
[JsonPropertyName("ownerModuleName")]
public string OwnerModuleName { get; set; }
// This is currently needed for FancyZones, we need to unify these two objects
// see src\common\settings_objects.h
[JsonPropertyName("key")]
@@ -120,9 +202,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
if (Shift)
{
shortcutList.Add("Shift");
// shortcutList.Add(16); // The Shift key or button.
shortcutList.Add(16); // The Shift key or button.
}
if (Code > 0)

View File

@@ -13,7 +13,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class MeasureToolProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, true, false, true, 0x4D);
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, true, false, true, 0x4D, "ActivationShortcut", MeasureToolSettings.ModuleName);
public MeasureToolProperties()
{

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class ModuleHotkeyInfo
{
public string ModuleName { get; set; }
public string HotkeyName { get; set; }
}
}

View File

@@ -11,7 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class MouseHighlighterProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x48);
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x48, "ActivationShortcut", MouseHighlighterSettings.ModuleName);
[JsonPropertyName("activation_shortcut")]
public HotkeySettings ActivationShortcut { get; set; }

View File

@@ -11,7 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class MouseJumpProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x44);
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x44, "ActivationShortcut", MouseJumpSettings.ModuleName);
[JsonPropertyName("activation_shortcut")]
public HotkeySettings ActivationShortcut

View File

@@ -11,7 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class MousePointerCrosshairsProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, true, false, 0x50); // Win + Alt + P
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, true, false, 0x50, "ActivationShortcut", MousePointerCrosshairsSettings.ModuleName); // Win + Alt + P
[JsonPropertyName("activation_shortcut")]
public HotkeySettings ActivationShortcut { get; set; }

View File

@@ -26,16 +26,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class MouseWithoutBordersProperties : ICloneable
{
[CmdConfigureIgnore]
public static HotkeySettings DefaultHotKeySwitch2AllPC => new HotkeySettings();
public static HotkeySettings DefaultHotKeySwitch2AllPC => new HotkeySettings("HotKeySwitch2AllPC", MouseWithoutBordersSettings.ModuleName);
[CmdConfigureIgnore]
public static HotkeySettings DefaultHotKeyLockMachine => new HotkeySettings(true, true, true, false, 0x4C);
public static HotkeySettings DefaultHotKeyLockMachine => new HotkeySettings(true, true, true, false, 0x4C, "HotKeyLockMachine", MouseWithoutBordersSettings.ModuleName);
[CmdConfigureIgnore]
public static HotkeySettings DefaultHotKeyReconnect => new HotkeySettings(true, true, true, false, 0x52);
public static HotkeySettings DefaultHotKeyReconnect => new HotkeySettings(true, true, true, false, 0x52, "HotKeyReconnect", MouseWithoutBordersSettings.ModuleName);
[CmdConfigureIgnore]
public static HotkeySettings DefaultHotKeyToggleEasyMouse => new HotkeySettings(true, true, true, false, 0x45);
public static HotkeySettings DefaultHotKeyToggleEasyMouse => new HotkeySettings(true, true, true, false, 0x45, "HotKeyToggleEasyMouse", MouseWithoutBordersSettings.ModuleName);
[CmdConfigureIgnore]
public StringProperty SecurityKey { get; set; }

View File

@@ -11,7 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class PeekProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(false, true, false, false, 0x20);
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(false, true, false, false, 0x20, "ActivationShortcut", PeekSettings.ModuleName);
public PeekProperties()
{

View File

@@ -95,20 +95,20 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public bool GenerateThumbnailsFromFiles { get; set; }
[CmdConfigureIgnoreAttribute]
public HotkeySettings DefaultOpenPowerLauncher => new HotkeySettings(false, false, true, false, 32);
public HotkeySettings DefaultOpenPowerLauncher => new HotkeySettings(false, false, true, false, 32, "OpenPowerLauncher", PowerLauncherSettings.ModuleName);
[CmdConfigureIgnoreAttribute]
public HotkeySettings DefaultOpenFileLocation => new HotkeySettings();
public HotkeySettings DefaultOpenFileLocation => new HotkeySettings("OpenFileLocation", PowerLauncherSettings.ModuleName);
[CmdConfigureIgnoreAttribute]
public HotkeySettings DefaultCopyPathLocation => new HotkeySettings();
public HotkeySettings DefaultCopyPathLocation => new HotkeySettings("CopyPathLocation", PowerLauncherSettings.ModuleName);
public PowerLauncherProperties()
{
OpenPowerLauncher = DefaultOpenPowerLauncher;
OpenFileLocation = DefaultOpenFileLocation;
CopyPathLocation = DefaultCopyPathLocation;
OpenConsole = new HotkeySettings();
OpenConsole = new HotkeySettings("OpenConsole", PowerLauncherSettings.ModuleName);
SearchResultPreference = "most_recently_used";
SearchTypePreference = "application_name";
IgnoreHotkeysInFullscreen = false;

View File

@@ -11,7 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class PowerOcrProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x54); // Win+Shift+T
public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, false, true, 0x54, "ActivationShortcut", PowerOcrSettings.ModuleName); // Win+Shift+T
public PowerOcrProperties()
{

View File

@@ -11,7 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public class ShortcutGuideProperties
{
[CmdConfigureIgnore]
public HotkeySettings DefaultOpenShortcutGuide => new HotkeySettings(true, false, false, true, 0xBF);
public HotkeySettings DefaultOpenShortcutGuide => new HotkeySettings(true, false, false, true, 0xBF, "OpenShortcutGuide", ShortcutGuideSettings.ModuleName);
public ShortcutGuideProperties()
{

View File

@@ -0,0 +1,20 @@
// 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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events
{
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class ShortcutConflictControlClickedEvent : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public int ConflictCount { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
// 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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events
{
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class ShortcutConflictResolvedEvent : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public bool HasConflict { get; set; }
public string ModuleName { get; set; }
public string HotkeyName { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
// 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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events
{
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class ShortcutConflictTestedEvent : EventBase, IEvent
{
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
public bool ConflictFound { get; set; }
public string ModuleName { get; set; }
public string HotkeyName { get; set; }
}
}

View File

@@ -16,7 +16,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Name,
}
public static readonly HotkeySettings DefaultHotkeyValue = new HotkeySettings(true, true, false, false, 0xC0);
public static readonly HotkeySettings DefaultHotkeyValue = new HotkeySettings(true, true, false, false, 0xC0, "Hotkey", WorkspacesSettings.ModuleName);
public WorkspacesProperties()
{

View File

@@ -14,25 +14,25 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
[CmdConfigureIgnore]
public static HotkeySettings DefaultToggleKey => new HotkeySettings(false, true, false, false, '1'); // Ctrl+1
public static HotkeySettings DefaultToggleKey => new HotkeySettings(false, true, false, false, '1', "ToggleKey", ZoomItSettings.ModuleName); // Ctrl+1
[CmdConfigureIgnore]
public static HotkeySettings DefaultLiveZoomToggleKey => new HotkeySettings(false, true, false, false, '4'); // Ctrl+4
public static HotkeySettings DefaultLiveZoomToggleKey => new HotkeySettings(false, true, false, false, '4', "LiveZoomToggleKey", ZoomItSettings.ModuleName); // Ctrl+4
[CmdConfigureIgnore]
public static HotkeySettings DefaultDrawToggleKey => new HotkeySettings(false, true, false, false, '2'); // Ctrl+2
public static HotkeySettings DefaultDrawToggleKey => new HotkeySettings(false, true, false, false, '2', "DrawToggleKey", ZoomItSettings.ModuleName); // Ctrl+2
[CmdConfigureIgnore]
public static HotkeySettings DefaultRecordToggleKey => new HotkeySettings(false, true, false, false, '5'); // Ctrl+5
public static HotkeySettings DefaultRecordToggleKey => new HotkeySettings(false, true, false, false, '5', "RecordToggleKey", ZoomItSettings.ModuleName); // Ctrl+5
[CmdConfigureIgnore]
public static HotkeySettings DefaultSnipToggleKey => new HotkeySettings(false, true, false, false, '6'); // Ctrl+6
public static HotkeySettings DefaultSnipToggleKey => new HotkeySettings(false, true, false, false, '6', "SnipToggleKey", ZoomItSettings.ModuleName); // Ctrl+6
[CmdConfigureIgnore]
public static HotkeySettings DefaultBreakTimerKey => new HotkeySettings(false, true, false, false, '3'); // Ctrl+3
public static HotkeySettings DefaultBreakTimerKey => new HotkeySettings(false, true, false, false, '3', "BreakTimerKey", ZoomItSettings.ModuleName); // Ctrl+3
[CmdConfigureIgnore]
public static HotkeySettings DefaultDemoTypeToggleKey => new HotkeySettings(false, true, false, false, '7'); // Ctrl+7
public static HotkeySettings DefaultDemoTypeToggleKey => new HotkeySettings(false, true, false, false, '7', "DemoTypeToggleKey", ZoomItSettings.ModuleName); // Ctrl+7
public KeyboardKeysProperty ToggleKey { get; set; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters
{
public partial class BoolNegationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool boolValue)
{
return !boolValue;
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
if (value is bool boolValue)
{
return !boolValue;
}
return value;
}
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters
{
public partial class BoolToConflictTypeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool isSystemConflict)
{
return isSystemConflict ? "System Conflict" : "In-App Conflict";
}
return "Unknown Conflict";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,36 @@
// 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.
// Add this file to: src/settings-ui/Settings.UI/Converters/BoolToVisibilityConverter.cs
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters
{
public partial class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool boolValue)
{
bool shouldInvert = parameter != null && parameter.ToString().Equals("Invert", StringComparison.OrdinalIgnoreCase);
bool result = shouldInvert ? !boolValue : boolValue;
return result ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
if (value is Visibility visibility)
{
return visibility == Visibility.Visible;
}
return false;
}
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters
{
public partial class ConditionalStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (parameter is string paramString && paramString.Contains('|'))
{
string[] parts = paramString.Split('|');
if (parts.Length == 2 && value is bool boolValue)
{
return boolValue ? parts[0] : parts[1];
}
}
return value?.ToString() ?? string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters
{
public partial class ConflictStyleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool hasConflict)
{
if (hasConflict)
{
return Application.Current.Resources["ConflictKeyVisualStyle"];
}
else
{
return Application.Current.Resources["AccentKeyVisualStyle"];
}
}
return Application.Current.Resources["AccentKeyVisualStyle"];
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters
{
public partial class IntToInvertedVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int intValue)
{
return intValue == 0 ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Converters
{
internal sealed partial class KeyVisualTemplateSelector : DataTemplateSelector
{
public DataTemplate KeyVisualTemplate { get; set; }
public DataTemplate CommaTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
var stringValue = item as string;
return stringValue == KeysDataModel.CommaSeparator ? CommaTemplate : KeyVisualTemplate;
}
}
}

View File

@@ -10,23 +10,17 @@ namespace Microsoft.PowerToys.Settings.UI.Converters
{
public partial class ModuleItemTemplateSelector : DataTemplateSelector
{
public DataTemplate TextTemplate { get; set; }
public DataTemplate ButtonTemplate { get; set; }
public DataTemplate ShortcutTemplate { get; set; }
public DataTemplate KBMTemplate { get; set; }
public DataTemplate ActivationTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
switch (item)
{
case DashboardModuleButtonItem: return ButtonTemplate;
case DashboardModuleShortcutItem: return ShortcutTemplate;
case DashboardModuleTextItem: return TextTemplate;
case DashboardModuleKBMItem: return KBMTemplate;
default: return TextTemplate;
case DashboardModuleActivationItem: return ActivationTemplate;
default: return ActivationTemplate;
}
}
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json.Nodes;
using Microsoft.PowerToys.Settings.UI.Library;
namespace Microsoft.PowerToys.Settings.UI.Helpers
{
public class HotkeyConflictHelper
{
public delegate void HotkeyConflictCheckCallback(bool hasConflict, HotkeyConflictResponse conflicts);
private static readonly Dictionary<string, HotkeyConflictCheckCallback> PendingHotkeyConflictChecks = new Dictionary<string, HotkeyConflictCheckCallback>();
private static readonly object LockObject = new object();
public static void CheckHotkeyConflict(HotkeySettings hotkeySettings, Func<string, int> ipcMSGCallBackFunc, HotkeyConflictCheckCallback callback)
{
if (hotkeySettings == null || ipcMSGCallBackFunc == null)
{
return;
}
string requestId = GenerateRequestId();
lock (LockObject)
{
PendingHotkeyConflictChecks[requestId] = callback;
}
var hotkeyObj = new JsonObject
{
["request_id"] = requestId,
["win"] = hotkeySettings.Win,
["ctrl"] = hotkeySettings.Ctrl,
["shift"] = hotkeySettings.Shift,
["alt"] = hotkeySettings.Alt,
["key"] = hotkeySettings.Code,
["moduleName"] = hotkeySettings.OwnerModuleName,
["hotkeyName"] = hotkeySettings.HotkeyName,
};
var requestObject = new JsonObject
{
["check_hotkey_conflict"] = hotkeyObj,
};
ipcMSGCallBackFunc(requestObject.ToString());
}
public static void HandleHotkeyConflictResponse(HotkeyConflictResponse response)
{
if (response.AllConflicts.Count == 0)
{
return;
}
HotkeyConflictCheckCallback callback = null;
lock (LockObject)
{
if (PendingHotkeyConflictChecks.TryGetValue(response.RequestId, out callback))
{
PendingHotkeyConflictChecks.Remove(response.RequestId);
}
}
callback?.Invoke(response.HasConflict, response);
}
private static string GenerateRequestId() => Guid.NewGuid().ToString();
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
namespace Microsoft.PowerToys.Settings.UI.Helpers
{
public class HotkeyConflictResponse
{
public string RequestId { get; set; }
public bool HasConflict { get; set; }
public List<ModuleHotkeyData> AllConflicts { get; set; } = new List<ModuleHotkeyData>();
}
}

View File

@@ -0,0 +1,216 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.PowerToys.Settings.UI.Helpers;
namespace Microsoft.PowerToys.Settings.UI.Helpers
{
internal sealed class LocalizationHelper
{
private static readonly Dictionary<(string ModuleName, string HotkeyName), string> HotkeyToResourceKeyMap = new()
{
// AdvancedPaste module mappings
{ ("advancedpaste", "AdvancedPasteUIShortcut"), "AdvancedPasteUI_Shortcut" },
{ ("advancedpaste", "PasteAsPlainTextShortcut"), "PasteAsPlainText_Shortcut" },
{ ("advancedpaste", "PasteAsMarkdownShortcut"), "PasteAsMarkdown_Shortcut" },
{ ("advancedpaste", "PasteAsJsonShortcut"), "PasteAsJson_Shortcut" },
{ ("advancedpaste", "ImageToTextShortcut"), "ImageToText" },
{ ("advancedpaste", "PasteAsTxtFileShortcut"), "PasteAsTxtFile" },
{ ("advancedpaste", "PasteAsPngFileShortcut"), "PasteAsPngFile" },
{ ("advancedpaste", "PasteAsHtmlFileShortcut"), "PasteAsHtmlFile" },
{ ("advancedpaste", "TranscodeToMp3Shortcut"), "TranscodeToMp3" },
{ ("advancedpaste", "TranscodeToMp4Shortcut"), "TranscodeToMp4" },
// AlwaysOnTop module mappings
{ ("alwaysontop", "Hotkey"), "AlwaysOnTop_ActivationShortcut" },
// ColorPicker module mappings
{ ("colorpicker", "ActivationShortcut"), "Activation_Shortcut" },
// CropAndLock module mappings
{ ("cropandlock", "ThumbnailHotkey"), "CropAndLock_ThumbnailActivation_Shortcut" },
{ ("cropandlock", "ReparentHotkey"), "CropAndLock_ReparentActivation_Shortcut" },
// MeasureTool module mappings
{ ("measuretool", "ActivationShortcut"), "MeasureTool_ActivationShortcut" },
// ShortcutGuide module mappings
{ ("shortcutguide", "OpenShortcutGuide"), "Activation_Shortcut" },
// PowerOCR/TextExtractor module mappings
{ ("textextractor", "ActivationShortcut"), "Activation_Shortcut" },
// Workspaces module mappings
{ ("workspaces", "Hotkey"), "Workspaces_ActivationShortcut" },
// Peek module mappings
{ ("peek", "ActivationShortcut"), "Activation_Shortcut" },
// PowerLauncher module mappings
{ ("powerlauncher", "OpenPowerLauncher"), "PowerLauncher_OpenPowerLauncher" },
// MouseUtils module mappings
{ ("mousehighlighter", "ActivationShortcut"), "MouseUtils_MouseHighlighter_ActivationShortcut" },
{ ("mousejump", "ActivationShortcut"), "MouseUtils_MouseJump_ActivationShortcut" },
{ ("mousepointercrosshairs", "ActivationShortcut"), "MouseUtils_MousePointerCrosshairs_ActivationShortcut" },
{ ("findmymouse", "ActivationShortcut"), "MouseUtils_FindMyMouse_ActivationShortcut" },
// Mouse without borders module mappings
{ ("mousewithoutborders", "HotKeySwitch2AllPC"), "MouseWithoutBorders_Switch2AllPcShortcut" },
{ ("mousewithoutborders", "HotKeyLockMachine"), "MouseWithoutBorders_LockMachinesShortcut" },
{ ("mousewithoutborders", "HotKeyReconnect"), "MouseWithoutBorders_ReconnectShortcut" },
{ ("mousewithoutborders", "HotKeyToggleEasyMouse"), "MouseWithoutBorders_ToggleEasyMouseShortcut" },
};
// Delegate for getting custom action names
public static Func<string, int, string> GetCustomActionNameDelegate { get; set; }
/// <summary>
/// Gets the localized header text based on module name and hotkey name
/// </summary>
/// <param name="moduleName">The name of the module (case-insensitive)</param>
/// <param name="hotkeyName">The name of the hotkey</param>
/// <returns>The localized header text, or the hotkey name if no resource is found</returns>
public static string GetLocalizedHotkeyHeader(string moduleName, string hotkeyName)
{
if (string.IsNullOrEmpty(moduleName) || string.IsNullOrEmpty(hotkeyName))
{
return hotkeyName ?? string.Empty;
}
var key = (moduleName.ToLowerInvariant(), hotkeyName);
// Try to get from resource file using resource key mapping
if (HotkeyToResourceKeyMap.TryGetValue(key, out string resourceKey))
{
var localizedText = GetLocalizedStringFromResource(resourceKey);
if (!string.IsNullOrEmpty(localizedText))
{
return localizedText;
}
}
// Handle custom actions for AdvancedPaste
if (moduleName.Equals("advancedpaste", StringComparison.OrdinalIgnoreCase) &&
hotkeyName.StartsWith("CustomAction_", StringComparison.OrdinalIgnoreCase))
{
// Try to get the custom action name using the delegate
if (GetCustomActionNameDelegate != null &&
int.TryParse(hotkeyName.AsSpan("CustomAction_".Length), out int actionId))
{
var customActionName = GetCustomActionNameDelegate(moduleName, actionId);
if (!string.IsNullOrEmpty(customActionName))
{
return customActionName;
}
}
// Fallback to resource
var customActionText = GetLocalizedStringFromResource("PasteAsCustom_Shortcut");
if (!string.IsNullOrEmpty(customActionText))
{
return customActionText;
}
}
// Try to generate resource key from hotkey name
var fallbackResourceKey = GenerateResourceKeyFromHotkeyName(moduleName, hotkeyName);
var fallbackText = GetLocalizedStringFromResource(fallbackResourceKey);
if (!string.IsNullOrEmpty(fallbackText))
{
return fallbackText;
}
// Final fallback: return the hotkey name as-is
return hotkeyName;
}
/// <summary>
/// Gets a localized string from the resource file using ResourceLoaderInstance
/// Tries multiple variations of the resource key to handle different naming conventions
/// </summary>
/// <param name="resourceKey">The resource key</param>
/// <returns>The localized string, or null if not found</returns>
private static string GetLocalizedStringFromResource(string resourceKey)
{
if (string.IsNullOrEmpty(resourceKey))
{
return null;
}
try
{
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
if (resourceLoader != null)
{
// Try different variations of the resource key
string[] keyVariations =
{
$"{resourceKey}.Header", // Try with .Header suffix first
resourceKey, // Try the key as-is
$"{resourceKey}/Header", // Try with /Header suffix (some resources use this format)
$"{resourceKey}_Header", // Try with _Header suffix
};
foreach (var keyVariation in keyVariations)
{
try
{
var result = resourceLoader.GetString(keyVariation);
if (!string.IsNullOrEmpty(result))
{
return result;
}
}
catch
{
// Continue to next variation
continue;
}
}
}
}
catch (Exception)
{
// If resource loading fails, return null to allow fallback
}
return null;
}
/// <summary>
/// Generates a resource key from module name and hotkey name
/// </summary>
/// <param name="moduleName">The module name</param>
/// <param name="hotkeyName">The hotkey name</param>
/// <returns>Generated resource key</returns>
private static string GenerateResourceKeyFromHotkeyName(string moduleName, string hotkeyName)
{
if (string.IsNullOrEmpty(moduleName) || string.IsNullOrEmpty(hotkeyName))
{
return string.Empty;
}
// Clean module name - capitalize first letter and make rest lowercase
var cleanModuleName = char.ToUpperInvariant(moduleName[0]) + moduleName.Substring(1).ToLowerInvariant();
// Clean hotkey name
string cleanHotkeyName = hotkeyName;
if (hotkeyName.EndsWith("Shortcut", StringComparison.OrdinalIgnoreCase))
{
cleanHotkeyName = hotkeyName.Substring(0, hotkeyName.Length - "Shortcut".Length);
}
else if (cleanHotkeyName.EndsWith("Hotkey", StringComparison.OrdinalIgnoreCase))
{
cleanHotkeyName = cleanHotkeyName.Substring(0, cleanHotkeyName.Length - "Hotkey".Length);
}
// Generate resource key pattern: ModuleName_HotkeyName_Shortcut
return $"{cleanModuleName}_{cleanHotkeyName}_Shortcut";
}
}
}

View File

@@ -0,0 +1,97 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.Views;
namespace Microsoft.PowerToys.Settings.UI.Helpers
{
public static class ModuleNavigationHelper
{
private static readonly Dictionary<string, Type> ModulePageMapping = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
{
{ "AdvancedPaste", typeof(AdvancedPastePage) },
{ "AlwaysOnTop", typeof(AlwaysOnTopPage) },
{ "ColorPicker", typeof(ColorPickerPage) },
{ "CropAndLock", typeof(CropAndLockPage) },
{ "MeasureTool", typeof(MeasureToolPage) },
{ "MouseHighlighter", typeof(MouseUtilsPage) },
{ "MouseJump", typeof(MouseUtilsPage) },
{ "MousePointerCrosshairs", typeof(MouseUtilsPage) },
{ "FindMyMouse", typeof(MouseUtilsPage) },
{ "MouseWithoutBorders", typeof(MouseWithoutBordersPage) },
{ "Peek", typeof(PeekPage) },
{ "PowerLauncher", typeof(PowerLauncherPage) },
{ "PowerOCR", typeof(PowerOcrPage) },
{ "ShortcutGuide", typeof(ShortcutGuidePage) },
{ "Workspaces", typeof(WorkspacesPage) },
// Shortcut conflict detection does not support the following modules.
{ "ZoomIt", typeof(ZoomItPage) },
{ "CmdPal", typeof(CmdPalPage) },
{ "FancyZones", typeof(FancyZonesPage) },
// The following modules do not have any shortcuts
{ "PowerPreview", typeof(PowerPreviewPage) },
{ "PowerRename", typeof(PowerRenamePage) },
{ "RegistryPreview", typeof(RegistryPreviewPage) },
{ "PowerAccent", typeof(PowerAccentPage) },
{ "NewPlus", typeof(NewPlusPage) },
{ "EnvironmentVariables", typeof(EnvironmentVariablesPage) },
{ "FileLocksmith", typeof(FileLocksmithPage) },
{ "Hosts", typeof(HostsPage) },
{ "ImageResizer", typeof(ImageResizerPage) },
{ "KeyboardManager", typeof(KeyboardManagerPage) },
{ "Awake", typeof(AwakePage) },
};
/// <summary>
/// Navigates to the settings page for the specified module
/// </summary>
/// <param name="moduleName">The name of the module</param>
/// <returns>True if navigation was successful, false otherwise</returns>
public static bool NavigateToModulePage(string moduleName)
{
if (string.IsNullOrEmpty(moduleName))
{
return false;
}
if (ModulePageMapping.TryGetValue(moduleName, out Type pageType))
{
return NavigationService.Navigate(pageType);
}
return false;
}
/// <summary>
/// Gets the page type for the specified module
/// </summary>
/// <param name="moduleName">The name of the module</param>
/// <returns>The page type if found, null otherwise</returns>
public static Type GetModulePageType(string moduleName)
{
if (string.IsNullOrEmpty(moduleName))
{
return null;
}
ModulePageMapping.TryGetValue(moduleName, out Type pageType);
return pageType;
}
/// <summary>
/// Checks if a module has a corresponding settings page
/// </summary>
/// <param name="moduleName">The name of the module</param>
/// <returns>True if the module has a settings page, false otherwise</returns>
public static bool HasModulePage(string moduleName)
{
return !string.IsNullOrEmpty(moduleName) && ModulePageMapping.ContainsKey(moduleName);
}
}
}

View File

@@ -22,6 +22,8 @@
<ItemGroup>
<None Remove="Assets\Settings\Modules\APDialog.dark.png" />
<None Remove="Assets\Settings\Modules\APDialog.light.png" />
<None Remove="SettingsXAML\Controls\Dashboard\CheckUpdateControl.xaml" />
<None Remove="SettingsXAML\Controls\Dashboard\ShortcutConflictControl.xaml" />
</ItemGroup>
<ItemGroup>
<Page Remove="SettingsXAML\App.xaml" />
@@ -132,6 +134,12 @@
<None Update="Assets\Settings\Scripts\DisableModule.ps1">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Page Update="SettingsXAML\Controls\Dashboard\ShortcutConflictControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="SettingsXAML\Controls\Dashboard\CheckUpdateControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
namespace Microsoft.PowerToys.Settings.UI.Services
{
public class GlobalHotkeyConflictManager
{
private readonly Func<string, int> _sendIPCMessage;
private static GlobalHotkeyConflictManager _instance;
private AllHotkeyConflictsData _currentConflicts = new AllHotkeyConflictsData();
public static GlobalHotkeyConflictManager Instance => _instance;
public static void Initialize(Func<string, int> sendIPCMessage)
{
_instance = new GlobalHotkeyConflictManager(sendIPCMessage);
}
private GlobalHotkeyConflictManager(Func<string, int> sendIPCMessage)
{
_sendIPCMessage = sendIPCMessage;
IPCResponseService.AllHotkeyConflictsReceived += OnAllHotkeyConflictsReceived;
}
public event EventHandler<AllHotkeyConflictsEventArgs> ConflictsUpdated;
public void RequestAllConflicts()
{
var requestMessage = "{\"get_all_hotkey_conflicts\":{}}";
_sendIPCMessage?.Invoke(requestMessage);
}
private void OnAllHotkeyConflictsReceived(object sender, AllHotkeyConflictsEventArgs e)
{
_currentConflicts = e.Conflicts;
ConflictsUpdated?.Invoke(this, e);
}
public bool HasConflictForHotkey(HotkeySettings hotkey, string moduleName = null, string hotkeyName = null)
{
if (hotkey == null)
{
return false;
}
var allConflictGroups = _currentConflicts.InAppConflicts.Concat(_currentConflicts.SystemConflicts);
foreach (var group in allConflictGroups)
{
if (IsHotkeyMatch(hotkey, group.Hotkey))
{
if (!string.IsNullOrEmpty(moduleName) && !string.IsNullOrEmpty(hotkeyName))
{
var selfModule = group.Modules.FirstOrDefault(m =>
m.ModuleName.Equals(moduleName, StringComparison.OrdinalIgnoreCase) &&
m.HotkeyName.Equals(hotkeyName, StringComparison.OrdinalIgnoreCase));
if (selfModule != null && group.Modules.Count == 1)
{
return false;
}
}
return true;
}
}
return false;
}
public HotkeyConflictInfo GetConflictInfo(HotkeySettings hotkey)
{
if (hotkey == null)
{
return null;
}
var allConflictGroups = _currentConflicts.InAppConflicts.Concat(_currentConflicts.SystemConflicts);
foreach (var group in allConflictGroups)
{
if (IsHotkeyMatch(hotkey, group.Hotkey))
{
var conflictModules = group.Modules.Where(m => m != null).ToList();
if (conflictModules.Count != 0)
{
var firstModule = conflictModules.First();
return new HotkeyConflictInfo
{
IsSystemConflict = group.IsSystemConflict,
ConflictingModuleName = firstModule.ModuleName,
ConflictingHotkeyName = firstModule.HotkeyName,
AllConflictingModules = conflictModules.Select(m => $"{m.ModuleName}:{m.HotkeyName}").ToList(),
};
}
}
}
return null;
}
private bool IsHotkeyMatch(HotkeySettings settings, HotkeyData data)
{
return settings.Win == data.Win &&
settings.Ctrl == data.Ctrl &&
settings.Shift == data.Shift &&
settings.Alt == data.Alt &&
settings.Code == data.Key;
}
}
}

View File

@@ -0,0 +1,199 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Views;
using Windows.Data.Json;
namespace Microsoft.PowerToys.Settings.UI.Services
{
public class IPCResponseService
{
private static IPCResponseService _instance;
public static IPCResponseService Instance => _instance ??= new IPCResponseService();
public static event EventHandler<AllHotkeyConflictsEventArgs> AllHotkeyConflictsReceived;
public void RegisterForIPC()
{
ShellPage.ShellHandler?.IPCResponseHandleList.Add(ProcessIPCMessage);
}
public void UnregisterFromIPC()
{
ShellPage.ShellHandler?.IPCResponseHandleList.Remove(ProcessIPCMessage);
}
private void ProcessIPCMessage(JsonObject json)
{
try
{
if (json.TryGetValue("response_type", out IJsonValue responseTypeValue) &&
responseTypeValue.ValueType == JsonValueType.String)
{
string responseType = responseTypeValue.GetString();
if (responseType.Equals("hotkey_conflict_result", StringComparison.Ordinal))
{
ProcessHotkeyConflictResult(json);
}
else if (responseType.Equals("all_hotkey_conflicts", StringComparison.Ordinal))
{
ProcessAllHotkeyConflicts(json);
}
}
}
catch (Exception)
{
}
}
private void ProcessHotkeyConflictResult(JsonObject json)
{
string requestId = string.Empty;
if (json.TryGetValue("request_id", out IJsonValue requestIdValue) &&
requestIdValue.ValueType == JsonValueType.String)
{
requestId = requestIdValue.GetString();
}
bool hasConflict = false;
if (json.TryGetValue("has_conflict", out IJsonValue hasConflictValue) &&
hasConflictValue.ValueType == JsonValueType.Boolean)
{
hasConflict = hasConflictValue.GetBoolean();
}
var allConflicts = new List<ModuleHotkeyData>();
if (hasConflict)
{
// Parse the all_conflicts array
if (json.TryGetValue("all_conflicts", out IJsonValue allConflictsValue) &&
allConflictsValue.ValueType == JsonValueType.Array)
{
var conflictsArray = allConflictsValue.GetArray();
foreach (var conflictItem in conflictsArray)
{
if (conflictItem.ValueType == JsonValueType.Object)
{
var conflictObj = conflictItem.GetObject();
string moduleName = string.Empty;
string hotkeyName = string.Empty;
if (conflictObj.TryGetValue("module", out IJsonValue moduleValue) &&
moduleValue.ValueType == JsonValueType.String)
{
moduleName = moduleValue.GetString();
}
if (conflictObj.TryGetValue("hotkey_name", out IJsonValue hotkeyValue) &&
hotkeyValue.ValueType == JsonValueType.String)
{
hotkeyName = hotkeyValue.GetString();
}
allConflicts.Add(new ModuleHotkeyData
{
ModuleName = moduleName,
HotkeyName = hotkeyName,
});
}
}
}
}
var response = new HotkeyConflictResponse
{
RequestId = requestId,
HasConflict = hasConflict,
AllConflicts = allConflicts,
};
HotkeyConflictHelper.HandleHotkeyConflictResponse(response);
}
private void ProcessAllHotkeyConflicts(JsonObject json)
{
var allConflicts = new AllHotkeyConflictsData();
if (json.TryGetValue("inAppConflicts", out IJsonValue inAppValue) &&
inAppValue.ValueType == JsonValueType.Array)
{
var inAppArray = inAppValue.GetArray();
foreach (var conflictGroup in inAppArray)
{
var conflictObj = conflictGroup.GetObject();
var conflictData = ParseConflictGroup(conflictObj, false);
if (conflictData != null)
{
allConflicts.InAppConflicts.Add(conflictData);
}
}
}
if (json.TryGetValue("sysConflicts", out IJsonValue sysValue) &&
sysValue.ValueType == JsonValueType.Array)
{
var sysArray = sysValue.GetArray();
foreach (var conflictGroup in sysArray)
{
var conflictObj = conflictGroup.GetObject();
var conflictData = ParseConflictGroup(conflictObj, true);
if (conflictData != null)
{
allConflicts.SystemConflicts.Add(conflictData);
}
}
}
AllHotkeyConflictsReceived?.Invoke(this, new AllHotkeyConflictsEventArgs(allConflicts));
}
private HotkeyConflictGroupData ParseConflictGroup(JsonObject conflictObj, bool isSystemConflict)
{
if (!conflictObj.TryGetValue("hotkey", out var hotkeyValue) ||
!conflictObj.TryGetValue("modules", out var modulesValue))
{
return null;
}
var hotkeyObj = hotkeyValue.GetObject();
bool win = hotkeyObj.TryGetValue("win", out var winVal) && winVal.GetBoolean();
bool ctrl = hotkeyObj.TryGetValue("ctrl", out var ctrlVal) && ctrlVal.GetBoolean();
bool shift = hotkeyObj.TryGetValue("shift", out var shiftVal) && shiftVal.GetBoolean();
bool alt = hotkeyObj.TryGetValue("alt", out var altVal) && altVal.GetBoolean();
int key = hotkeyObj.TryGetValue("key", out var keyVal) ? (int)keyVal.GetNumber() : 0;
var conflictGroup = new HotkeyConflictGroupData
{
Hotkey = new HotkeyData { Win = win, Ctrl = ctrl, Shift = shift, Alt = alt, Key = key },
IsSystemConflict = isSystemConflict,
Modules = new List<ModuleHotkeyData>(),
};
var modulesArray = modulesValue.GetArray();
foreach (var module in modulesArray)
{
var moduleObj = module.GetObject();
string moduleName = moduleObj.TryGetValue("moduleName", out var modNameVal) ? modNameVal.GetString() : string.Empty;
string hotkeyName = moduleObj.TryGetValue("hotkeyName", out var hotkeyNameVal) ? hotkeyNameVal.GetString() : string.Empty;
conflictGroup.Modules.Add(new ModuleHotkeyData
{
ModuleName = moduleName,
HotkeyName = hotkeyName,
});
}
return conflictGroup;
}
}
}

View File

@@ -3,17 +3,19 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml" />
<ResourceDictionary Source="/SettingsXAML/Controls/KeyVisual.xaml" />
<ResourceDictionary Source="/SettingsXAML/Styles/TextBlock.xaml" />
<ResourceDictionary Source="/SettingsXAML/Styles/Button.xaml" />
<ResourceDictionary Source="/SettingsXAML/Styles/InfoBadge.xaml" />
<ResourceDictionary Source="/SettingsXAML/Themes/Colors.xaml" />
<ResourceDictionary Source="/SettingsXAML/Themes/Generic.xaml" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
@@ -26,19 +28,28 @@
x:Key="BoolToVisibilityConverter"
FalseValue="Collapsed"
TrueValue="Visible" />
<tkconverters:BoolToObjectConverter
x:Key="BoolToComboBoxIndexConverter"
FalseValue="0"
TrueValue="1" />
<tkconverters:BoolToObjectConverter
x:Key="ReverseBoolToComboBoxIndexConverter"
FalseValue="1"
TrueValue="0" />
<tkconverters:DoubleToVisibilityConverter
x:Name="DoubleToVisibilityConverter"
FalseValue="Collapsed"
GreaterThan="0"
TrueValue="Visible" />
<tkconverters:DoubleToVisibilityConverter
x:Name="DoubleToInvertedVisibilityConverter"
FalseValue="Visible"
GreaterThan="0"
TrueValue="Collapsed" />
<tkconverters:StringFormatConverter x:Key="StringFormatConverter" />
<tkconverters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:UpdateStateToBoolConverter x:Key="UpdateStateToBoolConverter" />
<x:Double x:Key="SettingsCardSpacing">2</x:Double>
<!-- Overrides -->
@@ -46,6 +57,7 @@
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
<x:Double x:Key="SettingActionControlMinWidth">240</x:Double>
<x:Double x:Key="PageMaxWidth">1000</x:Double>
<Style TargetType="ListViewItem">
<Setter Property="Margin" Value="0,0,0,2" />

View File

@@ -232,6 +232,12 @@ namespace Microsoft.PowerToys.Settings.UI
});
ipcmanager.Start();
GlobalHotkeyConflictManager.Initialize(message =>
{
ipcmanager.Send(message);
return 0;
});
if (!ShowOobe && !ShowScoobe && !ShowFlyout)
{
settingsWindow = new MainWindow();
@@ -320,11 +326,20 @@ namespace Microsoft.PowerToys.Settings.UI
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(WindowNative.GetWindowHandle(settingsWindow));
settingsWindow.Activate();
settingsWindow.NavigateToSection(StartupPage);
// In DEBUG mode, we might not have IPC set up, so provide a dummy implementation
GlobalHotkeyConflictManager.Initialize(message =>
{
// In debug mode, just log or do nothing
System.Diagnostics.Debug.WriteLine($"IPC Message: {message}");
return 0;
});
ShowMessageDialog("The application is running in Debug mode.", "DEBUG");
#else
/* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */
Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard, true);
Exit();
/* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */
Common.UI.SettingsDeepLink.OpenSettings(Common.UI.SettingsDeepLink.SettingsWindow.Dashboard, true);
Exit();
#endif
}
}
@@ -436,6 +451,10 @@ namespace Microsoft.PowerToys.Settings.UI
case "KBM": return typeof(KeyboardManagerPage);
case "MouseUtils": return typeof(MouseUtilsPage);
case "MouseWithoutBorders": return typeof(MouseWithoutBordersPage);
case "Peek": return typeof(PeekPage);
case "PowerAccent": return typeof(PowerAccentPage);
case "PowerLauncher": return typeof(PowerLauncherPage);
case "PowerPreview": return typeof(PowerPreviewPage);
case "PowerRename": return typeof(PowerRenamePage);
case "QuickAccent": return typeof(PowerAccentPage);
case "FileExplorer": return typeof(PowerPreviewPage);
@@ -444,7 +463,6 @@ namespace Microsoft.PowerToys.Settings.UI
case "MeasureTool": return typeof(MeasureToolPage);
case "Hosts": return typeof(HostsPage);
case "RegistryPreview": return typeof(RegistryPreviewPage);
case "Peek": return typeof(PeekPage);
case "CropAndLock": return typeof(CropAndLockPage);
case "EnvironmentVariables": return typeof(EnvironmentVariablesPage);
case "NewPlus": return typeof(NewPlusPage);

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.PowerToys.Settings.UI.Controls.Card"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Padding="8"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource OverlayCornerRadius}"
mc:Ignorable="d">
<Grid
VerticalAlignment="{x:Bind VerticalContentAlignment, Mode=OneWay}"
Background="{x:Bind Background, Mode=OneWay}"
BorderBrush="{x:Bind BorderBrush, Mode=OneWay}"
BorderThickness="{x:Bind BorderThickness, Mode=OneWay}"
CornerRadius="{x:Bind CornerRadius, Mode=OneWay}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="44" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid x:Name="TitleGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Margin="16,0,0,0"
VerticalAlignment="Center"
FontSize="16"
FontWeight="SemiBold"
Text="{x:Bind Title, Mode=OneWay}" />
<ContentPresenter
Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{x:Bind TitleContent, Mode=OneWay}" />
</Grid>
<Rectangle
x:Name="Divider"
Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
Visibility="{x:Bind DividerVisibility, Mode=OneWay}" />
<ContentPresenter
Grid.Row="2"
Margin="{x:Bind Padding, Mode=OneWay}"
HorizontalAlignment="{x:Bind HorizontalContentAlignment, Mode=OneWay}"
VerticalAlignment="{x:Bind VerticalContentAlignment, Mode=OneWay}"
Content="{x:Bind Content, Mode=OneWay}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="TitleGridVisibilityStates">
<VisualState x:Name="TitleGridVisible" />
<VisualState x:Name="TitleGridCollapsed">
<VisualState.Setters>
<Setter Target="TitleGrid.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Controls
{
public sealed partial class Card : UserControl
{
public static readonly DependencyProperty TitleContentProperty = DependencyProperty.Register(nameof(TitleContent), typeof(object), typeof(Card), new PropertyMetadata(defaultValue: null, OnVisualPropertyChanged));
public object TitleContent
{
get => (object)GetValue(TitleContentProperty);
set => SetValue(TitleContentProperty, value);
}
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(Card), new PropertyMetadata(defaultValue: null, OnVisualPropertyChanged));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static new readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(Card), new PropertyMetadata(defaultValue: null));
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1061:Do not hide base class methods", Justification = "We need to hide the base class method")]
public new object Content
{
get => (object)GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
public static readonly DependencyProperty DividerVisibilityProperty = DependencyProperty.Register(nameof(DividerVisibility), typeof(Visibility), typeof(Card), new PropertyMetadata(defaultValue: null));
public Visibility DividerVisibility
{
get => (Visibility)GetValue(DividerVisibilityProperty);
set => SetValue(DividerVisibilityProperty, value);
}
public Card()
{
InitializeComponent();
SetVisualStates();
}
private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Card card)
{
card.SetVisualStates();
}
}
private void SetVisualStates()
{
if (string.IsNullOrEmpty(Title) && TitleContent == null)
{
VisualStateManager.GoToState(this, "TitleGridCollapsed", true);
DividerVisibility = Visibility.Collapsed;
}
else
{
VisualStateManager.GoToState(this, "TitleGridVisible", true);
DividerVisibility = Visibility.Visible;
}
}
}
}

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