Compare commits

..

198 Commits

Author SHA1 Message Date
Noraa Junker
dad8d00cda faster 2 2026-05-21 22:19:18 +02:00
Noraa Junker
1b71d64ef8 h 2026-05-21 19:28:38 +02:00
Noraa Junker
111d5b387e Try to make things faster 2026-05-21 19:25:12 +02:00
Noraa Junker
47d348f4ec Remove ZoomIt again, as it still does not work 2026-05-21 18:52:25 +02:00
Noraa Junker
4a71f79901 Fix missing PowerToys modules 2026-05-21 18:45:20 +02:00
Noraa Junker
79f69b87f2 Terminate more cleanly 2026-05-21 18:11:20 +02:00
Noraa Junker
6a0ff6a131 Fix wrong Shortcut Guide Shortcut 2026-05-21 17:47:42 +02:00
Noraa Junker
9123365993 Fix OOBE string 2026-05-21 17:36:14 +02:00
Niels Laute
82e4705ed6 Merge branch 'main' into feature/shortcutguidev2 2026-05-20 12:20:34 +02:00
Muyuan Li (from Dev Box)
cffddcdcb7 Revert personal website link change - not an issue per maintainer
Reverts the noraajunker.ch -> PR link change. As confirmed by @niels9001,
personal contributor links are standard in PowerToys settings pages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-20 15:24:06 +08:00
Muyuan Li (from Dev Box)
e7b6e36987 Resolve all PR #40834 review comments locally
Applied fixes:
- DpiHelper: int→nint for window handles (pointer truncation)
- DisplayHelper: static fields→lambda closure + bounds check
- NativeMethods: FindWindowA→FindWindowW (Unicode)
- MainWindow: MenuItems[0] guard, remove .ToInt32()
- TaskbarWindow: remove .ToInt32()
- Program.cs: logger init order, args handling, Environment.Exit(0)
- ShortcutDescription/ShortcutEntry: proper GetHashCode
- ShortcutDescriptionToKeysConverter: remove unused using
- excluded_app.cpp: m_excludedApps.clear() before parsing
- tasklist_positions: COM pointers from header to static in .cpp
- IndexYmlGenerator: O(n²) array rebuild→LINQ
- ShortcutGuidePage.xaml: personal link→GitHub PR link

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-20 15:05:06 +08:00
Niels Laute
c07a3d9bd9 Merge remote-tracking branch 'origin/main' into pr/40834 2026-05-19 12:02:29 +02:00
Niels Laute
dcfcd0a699 Fix CS0105: remove duplicate using ManagedCommon directive
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-19 12:02:21 +02:00
Niels Laute
7da07303b3 Update src/modules/ShortcutGuide/ShortcutGuide.Ui/Converters/ShortcutDescriptionToKeysConverter.cs
Co-authored-by: Muyuan Li <116717757+MuyuanMS@users.noreply.github.com>
2026-05-19 10:42:36 +02:00
Niels Laute
97d3b99bc5 Fix spellcheck: add ShortcutGuide V2 word variants, fix forbidden comma pattern
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-18 14:15:28 +02:00
Niels Laute
e0409b0271 Merge branch 'main' into feature/shortcutguidev2 2026-05-18 14:11:36 +02:00
Niels Laute
b58803642d Restore CmdPal files lost via rename-detection in earlier restore
These 7 files exist on main but were dropped by the bad merge. Git's
rename detection paired them with newly-resurrected files at different
paths, so the earlier --diff-filter=DM restore pass missed them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-17 19:40:37 +02:00
Niels Laute
87c91c5a2a Remove files resurrected by bad merge conflict resolution
These files were intentionally deleted on main by recent PRs but were
resurrected when 7aae7343aa merged main with a bad conflict resolution:
- HideWindowMessage.cs (deleted by #47826 CmdPal parameters)
- BookmarkPlaceholderForm.cs (deleted by #47886 bookmarks-as-parameters)
- NativeMethods.json/.txt + WindowsPackageManager.Interop/* (deleted by
  #46636 Extension Gallery)
- Ext.Indexer/Assets/Actions.png (moved/deleted by #46636)
- DdcCiValidationResult.cs (deleted by #47875 PowerDisplay max-compat)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-17 19:39:34 +02:00
Niels Laute
7716010a38 Restore files dropped by merge conflict resolution
Restores 114 deleted files and re-applies 129 file modifications that
were lost when a divergent merge of main into feature/shortcutguidev2
resolved conflicts by dropping recent CmdPal/Settings work from main
(ExtensionGallery, ListItemsView, ParametersViewModels, HttpCaching,
DangerousFeatureWarningDialog, etc.).

Source: tree of bf8493225d (the good merge sibling of 7aae7343aa).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-17 19:34:10 +02:00
Niels Laute
e77e7dfe68 Merge branch 'feature/shortcutguidev2' of https://github.com/microsoft/PowerToys into feature/shortcutguidev2 2026-05-17 19:27:25 +02:00
Niels Laute
7aae7343aa Merge branch 'main' into feature/shortcutguidev2 2026-05-17 19:27:21 +02:00
Niels Laute
bf8493225d Merge branch 'main' into feature/shortcutguidev2 2026-05-17 18:25:56 +01:00
Niels Laute
12eddcb320 More improvements 2026-05-16 19:31:23 +02:00
Niels Laute
9473657670 Update expect.txt 2026-05-13 22:10:23 +02:00
Niels Laute
9948a2831f Having a single list of shortcuts 2026-05-13 16:43:14 +02:00
Niels Laute
e746675ebf Move manifests to subfolder 2026-05-13 15:42:12 +02:00
Niels Laute
c3ec5b529b Logging 2026-05-13 14:50:05 +02:00
Niels Laute
6a8e439847 More minor refactoring 2026-05-13 14:47:34 +02:00
Niels Laute
36c30ae2fd Minor refactoring 2026-05-13 14:33:08 +02:00
Niels Laute
d09408ce43 Update Resources.resw 2026-05-13 13:32:08 +02:00
Niels Laute
ab79ae7477 Merge branch 'main' into feature/shortcutguidev2 2026-05-13 13:30:07 +02:00
Niels Laute
b6eb027558 Adding more shortcut files 2026-05-13 13:22:23 +02:00
Copilot
999ba28e34 Normalize spell-check expect list with case-sensitive dedupe and stable sort (#47112)
## Summary of the Pull Request

This updates the spell-check allowlist at
`.github/actions/spell-check/expect.txt` to remove duplicate entries and
enforce a single alphabetical ordering. Matching remains case-sensitive
(e.g., `AAA` and `aaa` are treated as distinct entries).

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

- **Scope**
  - Updated only `.github/actions/spell-check/expect.txt`.

- **Changes**
  - Removed exact duplicate words.
  - Reordered file into strict alphabetical order.
  - Preserved case-sensitive distinctions.

- **Example**
  ```bash
LC_ALL=C sort -u .github/actions/spell-check/expect.txt -o
.github/actions/spell-check/expect.txt
  ```

## Validation Steps Performed

N/A (no behavioral/code-path changes; list normalization only).

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: noraa-junker <58633848+noraa-junker@users.noreply.github.com>
2026-04-20 14:35:52 +02:00
Noraa Junker
03ff49601d Update expect.txt 2026-04-20 14:14:18 +02:00
Noraa Junker
406b42db64 Update expect.txt 2026-04-20 14:03:48 +02:00
Noraa Junker
8d4a24cd3f Merge branch 'main' into feature/shortcutguidev2 2026-04-20 13:57:22 +02:00
copilot-swe-agent[bot]
0a20b7efb5 Add spelling exceptions for ShortcutGuide-related words to expect.txt
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/9b871785-a2b4-4e2b-8fc0-d19b49fb0792

Co-authored-by: noraa-junker <58633848+noraa-junker@users.noreply.github.com>
2026-04-20 11:53:26 +00:00
Noraa Junker
bf18e7815b Correct postion logic 2026-04-20 13:49:10 +02:00
copilot-swe-agent[bot]
43b37bc733 Add window position dropdown (Left/Right) to Shortcut Guide settings
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/3883a5cc-f63f-4d66-8d5f-a73a94b9d86a

Co-authored-by: noraa-junker <58633848+noraa-junker@users.noreply.github.com>
2026-04-20 11:38:30 +00:00
Niels Laute
c011b2223c More shortcuts MORE! 2026-04-17 17:51:18 +02:00
Niels Laute
019ee046da Update Resources.resw 2026-04-17 17:45:48 +02:00
Niels Laute
e8579c39d0 Showing app icons 2026-04-17 17:14:29 +02:00
Niels Laute
911b39256c Fix crashing on launch 2026-04-17 16:24:46 +02:00
Noraa Junker
250a8cb5b7 Merge main 2026-04-01 12:53:28 +02:00
Noraa Junker
9f9d221eab Adjust docs 2026-02-02 01:40:19 +01:00
Noraa Junker
b9c5c15be2 Fix taskbar indicator to work with the latest canary builds 2026-02-02 01:35:08 +01:00
Noraa Junker
3a4f9c2274 Move cpp project files to powertoys.interop 2026-02-02 00:14:14 +01:00
Noraa Junker
a51fe19f82 merge main and adapt to new modules (aswell fix no taskbar bug) 2026-02-01 22:46:35 +01:00
Noraa Junker
efc814a610 merge main 2026-02-01 12:10:14 +01:00
Noraa Junker
1d1ae0d191 Merge main 2025-10-31 11:32:22 +01:00
Noraa Junker
3df4b45849 Merge remote-tracking branch 'origin/main' into feature/shortcutguidev2 2025-10-29 20:21:45 +01:00
Noraa Junker
88357a5a99 Fixed spawning on wrong display and taskbar window not working under certain circumstances on secondary displays 2025-10-29 20:21:20 +01:00
Noraa Junker
b4773affa7 Fix installer 2025-10-28 21:47:11 +01:00
Noraa Junker
e82c2d20cb Fix spelling again 2025-10-28 14:59:33 +01:00
Noraa Junker
e3a1f97ef3 Fix spelling 2025-10-28 14:51:48 +01:00
Noraa Junker
b26ded5370 Fix spelling 2025-10-28 14:51:36 +01:00
Noraa Junker
2e8ad4827b Fix some spelling issues 2025-10-28 14:43:54 +01:00
Noraa Junker
877626ef45 Added manifest file in 2025-10-28 14:30:41 +01:00
Noraa Junker
e0f72df36c Update ShortcutGuide image 2025-10-28 10:18:40 +01:00
Noraa Junker
552b02d596 Fix spawning window on the right monitor 2025-10-28 00:25:08 +01:00
Noraa Junker
c184acbada Fix xaml styling 2025-10-27 23:42:45 +01:00
Noraa Junker
55038c3c5e Fix missing localization on settings button 2025-10-27 23:42:26 +01:00
Noraa Junker
f537c43139 Added right key visuals and fixed some UI bugs 2025-10-27 23:36:17 +01:00
Noraa Junker
f867323677 Update OOBE string, supress window resizing and fix auto closing when taskbar window opened 2025-10-25 21:01:39 +02:00
Noraa Junker
d58145eb8c add zoomit shortcuts comment 2025-10-25 12:07:15 +02:00
Noraa Junker
486bec0ebd Fix empty windows key 2025-10-18 23:26:53 +02:00
Noraa Junker
e76506dffd Localization 2025-10-18 23:26:16 +02:00
Noraa Junker
841a5c5555 Fixed (un)pinning shortcuts 2025-10-18 23:04:47 +02:00
Noraa Junker
f646e0328e Fix wrong number for Taskbar item 10 2025-10-18 22:06:58 +02:00
Noraa Junker
5054a776dd Fix solution merge error 2025-10-18 22:06:46 +02:00
Noraa Junker
9d480c8e2c Merge conflicts 2025-10-18 21:51:43 +02:00
Niels Laute
d652285a81 [UX] Vertical shortcutguide (#41161)
- Exploring a vertical version for Shortcut Guide 2, to make better use
of the screen real estate
- Cleanup code and improved maintainability

<img width="1391" height="1439" alt="image"
src="https://github.com/user-attachments/assets/7ee3c925-71f1-46ee-83f6-4bc43b69db4c"
/>

<img width="715" height="1065" alt="image"
src="https://github.com/user-attachments/assets/e59684b2-2063-453e-93c7-df770eaa6999"
/>


To do:
- Shortcut visualizations are broken (well.. sometimes!)
- A lot of UX nits

---------

Co-authored-by: Aaron Junker <Aaron.Junker@outlook.com>
2025-10-17 14:39:35 +02:00
Aaron Junker
200afb5c4b merge main 2025-08-20 21:15:49 +02:00
Gordon Lam (SH)
e7582ebd6a Add back empty line which is missing during rebase main 2025-08-19 08:24:08 +08:00
Gordon Lam (SH)
b9c1181d9f Add back missing project because of rebase from main: 2025-08-19 08:16:20 +08:00
Aaron Junker
4aff3418e4 Change display name of File Explorer, hide taskbar indicators on other pages then Windows -> Overview and fix height of Windows -> Overview 2025-08-19 08:09:35 +08:00
Aaron Junker
e53e1b4376 fix xaml styling 2025-08-19 08:09:35 +08:00
Aaron Junker
73f718c233 git messed up 2025-08-19 08:09:34 +08:00
Aaron Junker
0917a64e7d close infotip when link is clicked and remove unneccesairy grid 2025-08-19 08:09:34 +08:00
Aaron Junker
a764bf3e0c Change oobe design 2025-08-19 08:09:34 +08:00
Aaron Junker
4853bd0345 Change some StaticResources to ThemeResources and change style of taskbar indicators 2025-08-19 08:09:34 +08:00
Aaron Junker
dceb1d7730 Fix styling 2025-08-19 08:09:34 +08:00
Aaron Junker
72be09554e Factor out TaskbarIndicator 2025-08-19 08:09:34 +08:00
Aaron Junker
19f95066c3 Fix xaml styling 2025-08-19 08:09:34 +08:00
Aaron Junker
bb16ae1709 Adress some PR comments and fix some bugs 2025-08-19 08:09:34 +08:00
Aaron Junker
dc0877ebe5 Make some adjustants to how the windows key is displayed 2025-08-19 08:09:34 +08:00
Aaron Junker
cd844e3889 Forgot notice.md and CPPProject has some trouble buildinng release x64 2025-08-19 08:09:34 +08:00
Aaron Junker
ac789a7fbe Remove failing test 2025-08-19 08:09:34 +08:00
Aaron Junker
3b7df37ac2 That is a kinda embarrasing error 2025-08-19 08:09:34 +08:00
Aaron Junker
387b7e9795 So something was not right. Hopefully this fixes it. 2025-08-19 08:09:34 +08:00
Aaron Junker
b553addcdd Fix arm64 configuration 2025-08-19 08:09:28 +08:00
Aaron Junker
5c11c751fe Fix xaml styling 2025-08-19 08:05:58 +08:00
Aaron Junker
6d7d5f9cde Fix building in release mode and some other stuff 2025-08-19 08:05:58 +08:00
Aaron Junker
4cb9c53809 Add documentation and only export in tasklist_positions what needs to be exported 2025-08-19 08:05:58 +08:00
Aaron Junker
84d4cbb16d Refactoring, commenting and fixing some little lefrover bugs 2025-08-19 08:05:49 +08:00
Aaron Junker
97cba618da Add explorer shortcuts, fix animation stopping and add an error when index.yml generation fails 2025-08-19 08:05:26 +08:00
Aaron Junker
145247c4fb Add attribution in settings 2025-08-19 08:04:42 +08:00
Aaron Junker
84ab12027b Add welcome screen and update settings and OOBE 2025-08-19 08:04:42 +08:00
Aaron Junker
3458d01d4c Respect excluded apps 2025-08-19 08:04:41 +08:00
Aaron Junker
2e6f80f944 Respect theme selection 2025-08-19 08:04:41 +08:00
Aaron Junker
f8cc513f9c Localization 2025-08-19 08:04:41 +08:00
Aaron Junker
b1d5233622 Fix spelling 2025-08-19 08:04:41 +08:00
Aaron Junker
68b7b4183f Make UI better 2025-08-19 08:04:41 +08:00
Aaron Junker
68a10d0488 Add disclaimers 2025-08-19 08:04:41 +08:00
Aaron Junker
e70ca56e9d Add taskbar launch shortcuts and make powertoys shortcuts empty by default 2025-08-19 08:04:34 +08:00
Aaron Junker
16c4a56ca1 Make settings button work and add settings placeholder 2025-08-19 08:02:30 +08:00
Aaron Junker
0d5c85a00d Add taskbar launch shortcuts and make powertoys shortcuts empty by default 2025-08-19 08:02:29 +08:00
Aaron Junker
509ad636fe Fix spelling 2025-08-19 08:02:29 +08:00
Aaron Junker
26f76105d4 Delete weird file 2025-08-19 08:02:29 +08:00
Aaron Junker
639b29eb8c Fix pinning and unpinning shortcuts 2025-08-19 08:02:29 +08:00
Aaron Junker
bff3874b5f Only display powertoys shortcuts if the modules are enabled 2025-08-19 08:02:29 +08:00
Aaron Junker
eff58e1df5 Fix closing by shortcut add closing by ESC and fix missing files from CPPProject 2025-08-19 08:02:29 +08:00
Aaron Junker
411f4df2c0 Remove legacy shortcut behaviour 2025-08-19 08:02:29 +08:00
Aaron Junker
7dc8c1000b Remove old Shortcut Guide 2025-08-19 08:02:23 +08:00
Aaron Junker
48d8e33375 Refactoring 2025-08-19 08:00:02 +08:00
Aaron Junker
afc27e873f Remove some hosts references and fix close button 2025-08-19 08:00:02 +08:00
Aaron Junker
3302e61d72 Refactoring and localisation 2025-08-19 08:00:02 +08:00
Aaron Junker
e6edca93e7 Add taskbar indicators 2025-08-19 07:59:56 +08:00
Aaron Junker
7acab452d5 Handle errors displaying app and close window automatically on focus change 2025-08-19 07:59:29 +08:00
Aaron Junker
0a07811233 Add keyboard accelerator to the search box 2025-08-19 07:59:29 +08:00
Aaron Junker
9ecf82d2ea Add copying keyboard manifests and other improvements 2025-08-19 07:59:19 +08:00
Aaron Junker
44d12c6e63 Add support for multiple shortcuts 2025-08-19 07:58:42 +08:00
Aaron Junker
6b8a3e65f7 push 2025-08-19 07:58:42 +08:00
Aaron Junker
2b16068a7d Rename YmlInterpreter to ManifestInterpreter 2025-08-19 07:58:42 +08:00
Aaron Junker
440e75184a Fix error messages and read application titles out of index manifest 2025-08-19 07:58:42 +08:00
Aaron Junker
acf510dff5 Fix display on monitor with mouse and move all NaticeMethods to NativeMethods.cs 2025-08-19 07:58:42 +08:00
Aaron Junker
ddd090cc81 Code cleanup 2025-08-19 07:58:42 +08:00
Aaron Junker
2f4766df19 Push 2025-08-19 07:58:42 +08:00
Aaron Junker
6558260c53 Push 2025-08-19 07:58:42 +08:00
Aaron Junker
55b3e15f10 Changed style a little bit 2025-08-19 07:58:42 +08:00
Aaron Junker
69c6475e15 [WIP] Shortcut Guide V2 2025-08-19 07:58:21 +08:00
Aaron Junker
8e7be164a9 Change display name of File Explorer, hide taskbar indicators on other pages then Windows -> Overview and fix height of Windows -> Overview 2025-08-07 11:57:04 +02:00
Aaron Junker
f42b3922c7 fix xaml styling 2025-08-05 10:33:42 +02:00
Aaron Junker
d568d16560 git messed up 2025-08-05 00:06:07 +02:00
Aaron Junker
0abae1d190 close infotip when link is clicked and remove unneccesairy grid 2025-08-04 23:51:18 +02:00
Aaron Junker
271e0c0533 Change oobe design 2025-08-04 23:34:23 +02:00
Aaron Junker
6bd5c4c811 Change some StaticResources to ThemeResources and change style of taskbar indicators 2025-08-03 22:37:40 +02:00
Aaron Junker
a41be807a4 Fix styling 2025-08-03 16:31:57 +02:00
Aaron Junker
e11626550e Factor out TaskbarIndicator 2025-08-03 16:30:59 +02:00
Aaron Junker
7266745124 Fix xaml styling 2025-08-01 17:53:23 +02:00
Aaron Junker
77a5bc2ff5 Adress some PR comments and fix some bugs 2025-08-01 17:51:32 +02:00
Aaron Junker
0b6683eb34 Make some adjustants to how the windows key is displayed 2025-08-01 17:06:54 +02:00
Aaron Junker
3796fdb706 Forgot notice.md and CPPProject has some trouble buildinng release x64 2025-07-30 18:17:26 +02:00
Aaron Junker
2d12932e44 Remove failing test 2025-07-30 17:31:31 +02:00
Aaron Junker
1da76e55bb Merge branch 'feature/shortcutguidev2' of https://github.com/microsoft/PowerToys into feature/shortcutguidev2 2025-07-30 17:21:46 +02:00
Aaron Junker
3c1a6a5b16 That is a kinda embarrasing error 2025-07-30 17:21:37 +02:00
Noraa Junker-Wildi
3b77feb879 Merge branch 'main' into feature/shortcutguidev2 2025-07-30 16:58:28 +02:00
Aaron Junker
7e7bb04d48 So something was not right. Hopefully this fixes it. 2025-07-30 16:50:20 +02:00
Aaron Junker
273b50cb16 Fix arm64 configuration 2025-07-29 18:51:21 +02:00
Aaron Junker
498c8d534f Fix xaml styling 2025-07-29 18:01:23 +02:00
Aaron Junker
0e2f466454 Fix building in release mode and some other stuff 2025-07-29 17:48:28 +02:00
Aaron Junker
1982f2615d Add documentation and only export in tasklist_positions what needs to be exported 2025-07-29 15:37:48 +02:00
Aaron Junker
2f89281178 Refactoring, commenting and fixing some little lefrover bugs 2025-07-28 16:12:36 +02:00
Aaron Junker
56f056e492 Add explorer shortcuts, fix animation stopping and add an error when index.yml generation fails 2025-07-28 14:08:42 +02:00
Aaron Junker
71dd8fe83f Add attribution in settings 2025-07-28 02:07:14 +02:00
Aaron Junker
f9183af53d Add welcome screen and update settings and OOBE 2025-07-28 02:02:55 +02:00
Aaron Junker
0f85f8bad6 Respect excluded apps 2025-07-27 23:48:49 +02:00
Aaron Junker
e0e7bf4df2 Respect theme selection 2025-07-27 22:45:14 +02:00
Aaron Junker
b12fcf6699 Localization 2025-07-27 22:06:15 +02:00
Aaron Junker
2ee02c4bbe Fix spelling 2025-07-27 21:12:12 +02:00
Aaron Junker
a306797d21 Make UI better 2025-07-27 21:11:16 +02:00
Aaron Junker
7e50caa04e Add disclaimers 2025-07-27 20:43:27 +02:00
Aaron Junker
e3b1ec356e commit 2025-07-27 20:30:03 +02:00
Aaron Junker
fa54b49fca Make settings button work and add settings placeholder 2025-07-27 20:29:36 +02:00
Aaron Junker
7e2fc4481d Add taskbar launch shortcuts and make powertoys shortcuts empty by default 2025-07-27 20:15:01 +02:00
Aaron Junker
201a27d2bb Add taskbar launch shortcuts and make powertoys shortcuts empty by default 2025-07-27 20:09:54 +02:00
Aaron Junker
fe3d481407 Fix spelling 2025-07-27 19:48:55 +02:00
Aaron Junker
3475c92f32 Delete weird file 2025-07-27 19:42:18 +02:00
Aaron Junker
82b0ca71fd Fix pinning and unpinning shortcuts 2025-07-27 19:39:43 +02:00
Aaron Junker
3a8431ae9d Only display powertoys shortcuts if the modules are enabled 2025-07-27 18:48:32 +02:00
Aaron Junker
eed6dc6f8d Merge branch 'feature/shortcutguidev2' of https://github.com/microsoft/PowerToys into feature/shortcutguidev2 2025-07-27 18:17:41 +02:00
Aaron Junker
038cd23423 Merge branch 'feature/shortcutguidev2' of https://github.com/microsoft/PowerToys into feature/shortcutguidev2 2025-07-27 18:17:39 +02:00
Aaron Junker
be799ddd82 Merge branch 'feature/shortcutguidev2' of https://github.com/microsoft/PowerToys into feature/shortcutguidev2 2025-07-27 18:00:52 +02:00
Aaron Junker
e26bf2acd6 Merge branch 'feature/shortcutguidev2' of https://github.com/microsoft/PowerToys into feature/shortcutguidev2 2025-07-27 18:00:49 +02:00
Aaron Junker
95e0a20444 Merge branch 'feature/shortcutguidev2' of https://github.com/microsoft/PowerToys into feature/shortcutguidev2 2025-07-27 17:47:27 +02:00
Aaron Junker
3e75fb0c52 Merge main in 2025-07-27 17:46:58 +02:00
Aaron Junker
ba4098960c Merge main in 2025-07-27 17:46:48 +02:00
Aaron Junker
1580279be1 Fix closing by shortcut add closing by ESC and fix missing files from CPPProject 2025-07-27 17:42:41 +02:00
Aaron Junker
07760e4730 Remove legacy shortcut behaviour 2025-07-27 16:59:25 +02:00
Aaron Junker
d73ab4f2d3 Remove old Shortcut Guide 2025-07-27 16:40:44 +02:00
Aaron Junker
a6b761433e Refactoring 2025-06-18 21:05:34 +02:00
Aaron Junker
da77396da5 Remove some hosts references and fix close button 2025-06-15 23:20:26 +02:00
Aaron Junker
46df48684d Refactoring and localisation 2025-06-15 22:13:41 +02:00
Aaron Junker
7596e965ef Add taskbar indicators 2025-06-13 23:37:54 +02:00
Aaron Junker
0b823ea8bd Handle errors displaying app and close window automatically on focus change 2025-06-11 15:41:39 +02:00
Aaron Junker
54d176c4c4 Add keyboard accelerator to the search box 2025-06-11 15:33:57 +02:00
Aaron Junker
569f07268b Add copying keyboard manifests and other improvements 2025-06-11 15:26:34 +02:00
Aaron Junker
7ce5182695 Add support for multiple shortcuts 2025-06-07 02:21:34 +02:00
Aaron Junker
b16b4579fa push 2025-06-06 23:32:40 +02:00
Aaron Junker
a292a92f4d Merge main 2025-06-06 22:20:01 +02:00
Aaron Junker
6952deb4ae Rename YmlInterpreter to ManifestInterpreter 2024-11-10 15:06:36 +01:00
Aaron Junker
da7b789bfe Fix error messages and read application titles out of index manifest 2024-11-10 15:05:34 +01:00
Aaron Junker
a14c458f19 Fix display on monitor with mouse and move all NaticeMethods to NativeMethods.cs 2024-11-04 00:17:43 +01:00
Aaron Junker
22dc870991 Code cleanup 2024-11-04 00:11:16 +01:00
Aaron Junker
d5f5500347 Push 2024-11-03 22:06:38 +01:00
Aaron Junker
c81a423880 Push 2024-11-02 15:10:33 +01:00
Aaron Junker
7a6189ba3e Changed style a little bit 2024-10-14 20:37:55 +02:00
Aaron Junker
03587ae800 [WIP] Shortcut Guide V2 2024-10-14 19:02:22 +02:00
76 changed files with 1429 additions and 3418 deletions

View File

@@ -308,11 +308,8 @@ pwa
AOT
Aot
cswinrt
ify
rsp
TFM
RTIID
# YML
onefuzz
@@ -368,10 +365,7 @@ FILESYSONLY
URLIS
WAITTIMEOUT
DEFAULTTONEAREST
DWRITE
LWIN
VCENTER
VREDRAW
# COM/WinRT interface prefixes and type fragments
BAlt

View File

@@ -954,7 +954,6 @@ keynum
keyremaps
keyring
keyvault
kfull
KILLFOCUS
killrunner
kmph
@@ -1062,7 +1061,6 @@ lstrcmpi
lstrcpyn
lstrlen
LTEXT
LTM
LTRREADING
luid
LUMA

View File

@@ -391,7 +391,6 @@
"WinUI3Apps\\Google.Apis.Auth.dll",
"WinUI3Apps\\Google.Apis.Core.dll",
"WinUI3Apps\\Google.GenAI.dll",
"WinUI3Apps\\YamlDotNet.dll",
"boost_regex-vc143-mt-gd-x32-1_87.dll",
"boost_regex-vc143-mt-gd-x64-1_87.dll",

View File

@@ -48,11 +48,6 @@ foreach ($csprojFile in $csprojFilesArray) {
continue
}
# The PowerAccent.Common project does not target WinRT, so skip it
if ($csprojFile -like '*PowerAccent.Common.csproj') {
continue
}
$importExists = Test-ImportSharedCsWinRTProps -filePath $csprojFile
if (!$importExists) {
Write-Output "$csprojFile need to import 'Common.Dotnet.CsWinRT.props'."

View File

@@ -57,7 +57,6 @@
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
<Project Path="src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj" Id="8b5cfb38-ccba-40a8-ad7a-89c57b070884" />
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
<Project Path="src/common/updating/UnitTests/UpdatingUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-abcd-ef1234567890" />
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
</Folder>
<Folder Name="/common/interop/">
@@ -802,14 +801,6 @@
<Project Path="src/modules/peek/peek/peek.vcxproj" Id="a1425b53-3d61-4679-8623-e64a0d3d0a48" />
</Folder>
<Folder Name="/modules/PowerAccent/">
<Project Path="src/modules/poweraccent/PowerAccent.Common.UnitTests/PowerAccent.Common.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/poweraccent/PowerAccent.Common/PowerAccent.Common.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -1142,5 +1133,3 @@
<Project Path="src/Update/PowerToys.Update.vcxproj" Id="44ce9ae1-4390-42c5-bacc-0fd6b40aa203" />
<Project Path="tools/project_template/ModuleTemplate/ModuleTemplateCompileTest.vcxproj" Id="64a80062-4d8b-4229-8a38-dfa1d7497749" />
</Solution>

2
deps/spdlog vendored

View File

@@ -0,0 +1,94 @@
// spdlog-msvc-fix.h
//
// Workaround for MSVC 14.51 (compiler version 19.51, _MSC_VER >= 1951) removing
// stdext::checked_array_iterator. Force-included for all spdlog consumers via
// deps/spdlog.props, because spdlog v1.8.5's bundled fmt format.h(357) still
// references this type inside #if defined(_SECURE_SCL) && _SECURE_SCL -- a
// branch entered in Debug builds where _ITERATOR_DEBUG_LEVEL > 0.
//
// On MSVC 14.50 and earlier, the type still exists in <iterator>, so this shim
// is a no-op via the _MSC_VER guard. On MSVC 14.51+, it provides a minimal
// pointer-backed substitute that satisfies the bundled fmt's usage:
//
// template <typename T> using checked_ptr = stdext::checked_array_iterator<T*>;
// template <typename T> checked_ptr<T> make_checked(T* p, size_t size) {
// return {p, size};
// }
// ... return make_checked(get_data(c) + size, n);
//
// When deps/spdlog is bumped past v1.14 (which ships fmt 10.2 and drops this
// dependency), this shim and its <ForcedIncludeFiles> entry in deps/spdlog.props
// can be deleted.
#pragma once
#if defined(__cplusplus) && defined(_MSC_VER) && _MSC_VER >= 1951
#include <cstddef>
#include <iterator>
#include <type_traits>
namespace stdext
{
template <typename _Ptr>
class checked_array_iterator
{
_Ptr _Myarray = nullptr;
std::size_t _Mysize = 0;
std::size_t _Myindex = 0;
public:
using iterator_category = std::random_access_iterator_tag;
using value_type = std::remove_cv_t<std::remove_pointer_t<_Ptr>>;
using difference_type = std::ptrdiff_t;
using pointer = _Ptr;
using reference = std::remove_pointer_t<_Ptr>&;
constexpr checked_array_iterator() = default;
constexpr checked_array_iterator(_Ptr arr, std::size_t size, std::size_t idx = 0) noexcept
: _Myarray(arr), _Mysize(size), _Myindex(idx)
{
}
constexpr reference operator*() const noexcept { return _Myarray[_Myindex]; }
constexpr pointer operator->() const noexcept { return _Myarray + _Myindex; }
constexpr reference operator[](difference_type n) const noexcept
{
return _Myarray[_Myindex + static_cast<std::size_t>(n)];
}
constexpr checked_array_iterator& operator++() noexcept { ++_Myindex; return *this; }
constexpr checked_array_iterator operator++(int) noexcept { auto t = *this; ++_Myindex; return t; }
constexpr checked_array_iterator& operator--() noexcept { --_Myindex; return *this; }
constexpr checked_array_iterator operator--(int) noexcept { auto t = *this; --_Myindex; return t; }
constexpr checked_array_iterator& operator+=(difference_type n) noexcept
{
_Myindex = static_cast<std::size_t>(static_cast<difference_type>(_Myindex) + n);
return *this;
}
constexpr checked_array_iterator& operator-=(difference_type n) noexcept
{
_Myindex = static_cast<std::size_t>(static_cast<difference_type>(_Myindex) - n);
return *this;
}
friend constexpr checked_array_iterator operator+(checked_array_iterator it, difference_type n) noexcept { it += n; return it; }
friend constexpr checked_array_iterator operator+(difference_type n, checked_array_iterator it) noexcept { return it + n; }
friend constexpr checked_array_iterator operator-(checked_array_iterator it, difference_type n) noexcept { it -= n; return it; }
friend constexpr difference_type operator-(checked_array_iterator a, checked_array_iterator b) noexcept
{
return static_cast<difference_type>(a._Myindex) - static_cast<difference_type>(b._Myindex);
}
friend constexpr bool operator==(checked_array_iterator a, checked_array_iterator b) noexcept { return a._Myindex == b._Myindex; }
friend constexpr bool operator!=(checked_array_iterator a, checked_array_iterator b) noexcept { return !(a == b); }
friend constexpr bool operator<(checked_array_iterator a, checked_array_iterator b) noexcept { return a._Myindex < b._Myindex; }
friend constexpr bool operator>(checked_array_iterator a, checked_array_iterator b) noexcept { return b < a; }
friend constexpr bool operator<=(checked_array_iterator a, checked_array_iterator b) noexcept { return !(b < a); }
friend constexpr bool operator>=(checked_array_iterator a, checked_array_iterator b) noexcept { return !(a < b); }
};
} // namespace stdext
#endif // __cplusplus && _MSC_VER >= 1951

3
deps/spdlog.props vendored
View File

@@ -2,7 +2,8 @@
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)spdlog\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;FMT_UNICODE=0;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ForcedIncludeFiles>$(MSBuildThisFileDirectory)spdlog-msvc-fix\include\spdlog-msvc-fix.h;%(ForcedIncludeFiles)</ForcedIncludeFiles>
</ClCompile>
</ItemDefinitionGroup>
</Project>

View File

@@ -12,17 +12,6 @@
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
</PropertyGroup>
<!--
Opt out of CsWinRT 2.2 IIDOptimizer. On .NET 10 / CsWinRT 2.2 the tool exits with code -1
after producing "0 IID calculations/fetches patched", which generates a noisy MSB3073
warning for every CsWinRT-consuming project. The IIDOptimizer is a runtime-perf optimization
that interns GUID lookups; disabling it just means a small first-call cost. This switch
causes Microsoft.Windows.CsWinRT.IIDOptimizer.targets to not be imported at all.
-->
<PropertyGroup>
<CsWinRTIIDOptimizerOptOut>true</CsWinRTIIDOptimizerOptOut>
</PropertyGroup>
<!-- Common from the debug / release items -->
<PropertyGroup>
<WarningLevel>4</WarningLevel>
@@ -52,54 +41,7 @@
<!-- this may need to be removed on future CsWinRT upgrades-->
<Target Name="RemoveCsWinRTPackageAnalyzer" BeforeTargets="CoreCompile">
<ItemGroup>
<Analyzer Remove="@(Analyzer)" Condition="%(Analyzer.NuGetPackageId) == 'Microsoft.Windows.CsWinRT'" />
<Analyzer Remove="@(Analyzer)" Condition="%(Analyzer.NuGetPackageId) == 'Microsoft.Windows.CsWinRT'" />
</ItemGroup>
</Target>
<!--
Ensure any referenced C++/WinRT (.vcxproj) projects are fully built BEFORE the CsWinRT
source generator runs in this csproj. On a clean machine the SDK-style ProjectReference
graph does not guarantee that the producing vcxproj has emitted its .winmd before the
consuming C# Compile / source-generator stage starts in a parallel solution build,
which manifests as CS0246 on the projected namespace (e.g. 'PowerToys.Interop').
Forcing a serialized Build of the .vcxproj references here closes that race.
We hook BEFORE ResolveProjectReferences so the produced .winmd is visible to
CsWinRTRemoveWinMDReferences (which moves it into @(CsWinRTInputs)) and we also
delete a possibly stale cswinrt.rsp so CsWinRTGenerateProjection re-invokes
cswinrt.exe instead of incrementally skipping.
-->
<Target Name="EnsureNativeWinMDProjectionInputsBuilt"
BeforeTargets="ResolveProjectReferences;ResolveAssemblyReferences;CsWinRTPrepareProjection;CsWinRTGenerateProjection"
Condition="'@(ProjectReference)' != '' and '$(DesignTimeBuild)' != 'true' and '$(BuildingProject)' != 'false'">
<ItemGroup>
<_NativeWinMDProjectionRef Include="@(ProjectReference)" Condition="'%(Extension)' == '.vcxproj'" />
</ItemGroup>
<MSBuild Projects="@(_NativeWinMDProjectionRef)"
Properties="Configuration=$(Configuration);Platform=$(Platform)"
Targets="Build"
BuildInParallel="false"
Condition="'@(_NativeWinMDProjectionRef)' != ''" />
<!-- Force CsWinRTGenerateProjection to re-run so stale-rsp incremental skip cannot
leave us without generated .cs files when the .winmd has just been (re)produced. -->
<Delete Files="$(CsWinRTGeneratedFilesDir)cswinrt.rsp;$(CsWinRTGeneratedFilesDir)cswinrt_internal.rsp"
Condition="'@(_NativeWinMDProjectionRef)' != '' and '$(CsWinRTGeneratedFilesDir)' != ''" />
<!-- Mark that we need to delete the rsp again once CsWinRTGeneratedFilesDir is fully resolved
(some projects set it to $(OutDir) which is not evaluated this early). -->
<PropertyGroup>
<_DeleteStaleCsWinRTRspPending>true</_DeleteStaleCsWinRTRspPending>
</PropertyGroup>
</Target>
<!--
Second pass: after CsWinRTPrepareProjection has resolved $(CsWinRTGeneratedFilesDir) to its
final value (which may depend on $(OutDir)), delete any stale cswinrt.rsp so the
CsWinRTGenerateProjection target's incremental-skip cannot leave us without generated .cs files.
-->
<Target Name="DeleteStaleCsWinRTRspAfterPrepare"
AfterTargets="CsWinRTPrepareProjection"
BeforeTargets="CsWinRTGenerateProjection"
Condition="'$(_DeleteStaleCsWinRTRspPending)' == 'true' and '$(CsWinRTGeneratedFilesDir)' != ''">
<Delete Files="$(CsWinRTGeneratedFilesDir)cswinrt.rsp;$(CsWinRTGeneratedFilesDir)cswinrt_internal.rsp" />
</Target>
</Project>

View File

@@ -14,8 +14,6 @@
#include <common/updating/updating.h>
#include <common/updating/updateState.h>
#include <common/updating/installer.h>
#include <common/updating/configBackup.h>
#include <common/updating/updateLifecycle.h>
#include <common/utils/elevation.h>
#include <common/utils/HttpClient.h>
@@ -23,8 +21,6 @@
#include <common/utils/resources.h>
#include <common/utils/timeutil.h>
#include <wil/resource.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/logger/logger.h>
@@ -40,59 +36,17 @@ using namespace cmdArg;
namespace fs = std::filesystem;
void CleanupStaleTempUpdaters()
{
// Remove orphaned PowerToys.Update.*.exe files from previous runs
try
{
std::error_code ec;
const auto tempDir = fs::temp_directory_path();
for (const auto& entry : fs::directory_iterator(tempDir, ec))
{
if (ec)
{
break;
}
if (!entry.is_regular_file())
{
continue;
}
const auto filename = entry.path().filename().wstring();
if (filename.starts_with(L"PowerToys.Update.") && filename.ends_with(L".exe"))
{
// Skip our own file (current PID)
const auto ownFilename = L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe";
if (filename == ownFilename)
{
continue;
}
fs::remove(entry.path(), ec);
// Failure to delete is expected if another updater is still running
}
}
}
catch (...)
{
// Best-effort cleanup; don't block the update
}
}
std::optional<fs::path> CopySelfToTempDir()
{
CleanupStaleTempUpdaters();
std::error_code error;
auto dst_path = fs::temp_directory_path() / (L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe");
auto dst_path = fs::temp_directory_path() / "PowerToys.Update.exe";
fs::copy_file(get_module_filename(), dst_path, fs::copy_options::overwrite_existing, error);
if (error)
{
return std::nullopt;
}
return dst_path;
return std::move(dst_path);
}
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
@@ -103,9 +57,34 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
auto state = UpdateState::read();
// Handle readyToInstall first — the installer is already on disk,
// so we don't need a GitHub API call (which may fail if offline).
if (state.state == UpdateState::readyToInstall)
const auto new_version_info = std::move(get_github_version_info_async()).get();
if (std::holds_alternative<version_up_to_date>(*new_version_info))
{
isUpToDate = true;
Logger::error("Invoked with -update_now argument, but no update was available");
return std::nullopt;
}
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
{
if (!new_version_info)
{
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
return std::nullopt;
}
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
if (!downloaded_installer)
{
Logger::error("Couldn't download new installer");
}
return downloaded_installer;
}
else if (state.state == UpdateState::readyToInstall)
{
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
if (fs::is_regular_file(installer))
@@ -118,44 +97,12 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
return std::nullopt;
}
}
if (state.state == UpdateState::upToDate)
else if (state.state == UpdateState::upToDate)
{
isUpToDate = true;
return std::nullopt;
}
const auto new_version_info = std::move(get_github_version_info_async()).get();
// Check for error BEFORE dereferencing — the old code crashed here
// when GitHub API was unreachable (new_version_info held an error string).
if (!new_version_info)
{
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
return std::nullopt;
}
if (std::holds_alternative<version_up_to_date>(*new_version_info))
{
isUpToDate = true;
Logger::error("Invoked with -update_now argument, but no update was available");
return std::nullopt;
}
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
{
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
if (!downloaded_installer)
{
Logger::error("Couldn't download new installer");
}
return downloaded_installer;
}
Logger::error("Invoked with -update_now argument, but update state was invalid");
return std::nullopt;
}
@@ -169,32 +116,13 @@ bool InstallNewVersionStage1(fs::path installer)
if (pt_main_window != nullptr)
{
// Get the process that owns the tray window so we can wait for it to exit
DWORD ptProcessId = 0;
GetWindowThreadProcessId(pt_main_window, &ptProcessId);
// Use SendMessageTimeoutW to avoid blocking indefinitely if the
// tray window thread is hung or unresponsive.
DWORD_PTR result = 0;
SendMessageTimeoutW(pt_main_window, WM_CLOSE, 0, 0, SMTO_ABORTIFHUNG, 5000, &result);
// Wait for PT to actually exit before launching installer.
// Without this, the installer may find PT files locked.
if (ptProcessId != 0)
{
wil::unique_handle ptProcess{ OpenProcess(SYNCHRONIZE, FALSE, ptProcessId) };
if (ptProcess)
{
WaitForSingleObject(ptProcess.get(), 10000); // 10 second timeout
}
}
SendMessageW(pt_main_window, WM_CLOSE, 0, 0);
}
// Pass the install directory so Stage 2 can relaunch PowerToys after install
const std::wstring installDir = get_module_folderpath();
std::wstring arguments = updating::BuildStage2Arguments(
UPDATE_NOW_LAUNCH_STAGE2, installer, fs::path(installDir));
std::wstring arguments{ UPDATE_NOW_LAUNCH_STAGE2 };
arguments += L" \"";
arguments += installer.c_str();
arguments += L"\"";
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
sei.lpFile = copy_in_temp->c_str();
@@ -262,16 +190,9 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
LPWSTR* args = CommandLineToArgvW(GetCommandLineW(), &nArgs);
if (!args || nArgs < 2)
{
if (args)
{
LocalFree(args);
}
return 1;
}
// D3 fix: ensure args is freed on all exit paths
auto freeArgs = wil::scope_exit([&] { LocalFree(args); });
std::wstring_view action{ args[1] };
std::filesystem::path logFilePath(PTSettingsHelper::get_root_save_folder_location());
@@ -280,11 +201,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
if (action == UPDATE_NOW_LAUNCH_STAGE1)
{
// Backup config files before the update to protect against corruption
Logger::info("Backing up config files before update");
auto backupResult = updating::BackupConfigFiles(fs::path(PTSettingsHelper::get_root_save_folder_location()));
Logger::info("Config backup complete: {} files backed up, {} errors", backupResult.filesBackedUp, backupResult.errors);
bool isUpToDate = false;
auto installerPath = ObtainInstaller(isUpToDate);
bool failed = !installerPath.has_value();
@@ -301,12 +217,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
}
else if (action == UPDATE_NOW_LAUNCH_STAGE2)
{
if (nArgs < 3)
{
Logger::error("Stage 2 invoked without installer path argument");
return 1;
}
using namespace std::string_view_literals;
const bool failed = !InstallNewVersionStage2(args[2]);
if (failed)
@@ -317,39 +227,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
state.state = UpdateState::errorDownloading;
});
}
// Always check for corrupted configs after Stage 2, regardless
// of install success/failure. A failed install may still corrupt configs.
Logger::info("Checking for corrupted config files after update");
auto restoreResult = updating::RestoreCorruptedConfigs(fs::path(PTSettingsHelper::get_root_save_folder_location()));
Logger::info("Config restore check complete: {}/{} files restored, {} errors",
restoreResult.filesRestored, restoreResult.filesChecked, restoreResult.errors);
if (!failed)
{
// Relaunch PowerToys from the install directory
if (updating::CanRelaunchAfterUpdate(nArgs))
{
std::wstring ptExePath = updating::BuildPowerToysExePath(args[3]);
Logger::info(L"Relaunching PowerToys after update: {}", ptExePath);
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
sei.lpFile = ptExePath.c_str();
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = UPDATE_REPORT_SUCCESS;
if (!ShellExecuteExW(&sei))
{
Logger::error(L"Failed to relaunch PowerToys after update");
}
}
else
{
Logger::warn("Install directory not provided to Stage 2 - cannot relaunch PowerToys");
}
}
return failed;
}

View File

@@ -161,7 +161,7 @@
<ClCompile>
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpp20</LanguageStandard>
<LanguageStandard Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT

View File

@@ -29,7 +29,7 @@
<ClCompile>
<AdditionalIncludeDirectories>..\;..\utils;..\Telemetry;..\..\;..\..\..\deps\;..\..\..\deps\spdlog\include;..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\include;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<LanguageStandard>stdcpp23</LanguageStandard>
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_HEADER_ONLY;FMT_UNICODE=0;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_HEADER_ONLY;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>

View File

@@ -1,27 +1,7 @@
#pragma once
#include <spdlog/spdlog.h>
#include <type_traits>
#include "logger_settings.h"
// fmt 9+ no longer auto-formats enums. Provide a generic formatter that
// converts any scoped or unscoped enum to its underlying integer type so
// existing Logger::xxx(L"... {} ...", someEnum) call sites keep working
// after the spdlog 1.17 / fmt 12 upgrade.
namespace fmt
{
template <typename E, typename Char>
struct formatter<E, Char, std::enable_if_t<std::is_enum_v<E>>>
: formatter<std::underlying_type_t<E>, Char>
{
template <typename FormatContext>
auto format(E value, FormatContext& ctx) const
{
return formatter<std::underlying_type_t<E>, Char>::format(
static_cast<std::underlying_type_t<E>>(value), ctx);
}
};
}
class Logger
{
private:
@@ -37,44 +17,44 @@ public:
// log message should not be localized
template<typename FormatString, typename... Args>
static void trace(const FormatString& formatString, const Args&... args)
static void trace(const FormatString& fmt, const Args&... args)
{
logger->trace(fmt::runtime(formatString), args...);
logger->trace(fmt, args...);
}
// log message should not be localized
template<typename FormatString, typename... Args>
static void debug(const FormatString& formatString, const Args&... args)
static void debug(const FormatString& fmt, const Args&... args)
{
logger->debug(fmt::runtime(formatString), args...);
logger->debug(fmt, args...);
}
// log message should not be localized
template<typename FormatString, typename... Args>
static void info(const FormatString& formatString, const Args&... args)
static void info(const FormatString& fmt, const Args&... args)
{
logger->info(fmt::runtime(formatString), args...);
logger->info(fmt, args...);
}
// log message should not be localized
template<typename FormatString, typename... Args>
static void warn(const FormatString& formatString, const Args&... args)
static void warn(const FormatString& fmt, const Args&... args)
{
logger->warn(fmt::runtime(formatString), args...);
logger->warn(fmt, args...);
}
// log message should not be localized
template<typename FormatString, typename... Args>
static void error(const FormatString& formatString, const Args&... args)
static void error(const FormatString& fmt, const Args&... args)
{
logger->error(fmt::runtime(formatString), args...);
logger->error(fmt, args...);
}
// log message should not be localized
template<typename FormatString, typename... Args>
static void critical(const FormatString& formatString, const Args&... args)
static void critical(const FormatString& fmt, const Args&... args)
{
logger->critical(fmt::runtime(formatString), args...);
logger->critical(fmt, args...);
}
static void flush()

View File

@@ -1,679 +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.
#include "pch.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
#include <common/updating/configBackup.h>
#include <common/updating/updateLifecycle.h>
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace fs = std::filesystem;
namespace UpdatingUnitTests
{
// Helper to create a temp directory for test isolation.
// Each instance gets a unique subdirectory to prevent test interference.
class TempDir
{
public:
TempDir()
{
wchar_t tempPath[MAX_PATH + 1];
GetTempPathW(MAX_PATH, tempPath);
static std::atomic<int> counter{0};
m_path = fs::path(tempPath) / (L"PowerToysUpdateTests_" + std::to_wstring(counter++));
// Ensure clean state
std::error_code ec;
fs::remove_all(m_path, ec);
fs::create_directories(m_path, ec);
}
~TempDir()
{
std::error_code ec;
fs::remove_all(m_path, ec);
}
const fs::path& path() const { return m_path; }
// Write a file with the given content
void WriteFile(const fs::path& relativePath, const std::string& content)
{
auto fullPath = m_path / relativePath;
fs::create_directories(fullPath.parent_path());
std::ofstream file(fullPath, std::ios::binary);
file.write(content.data(), content.size());
}
// Write a file with raw bytes (including null bytes for corruption testing)
void WriteFileBytes(const fs::path& relativePath, const std::vector<char>& bytes)
{
auto fullPath = m_path / relativePath;
fs::create_directories(fullPath.parent_path());
std::ofstream file(fullPath, std::ios::binary);
file.write(bytes.data(), bytes.size());
}
// Read file content as string
std::string ReadFile(const fs::path& relativePath)
{
auto fullPath = m_path / relativePath;
std::ifstream file(fullPath, std::ios::binary);
return std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
}
bool FileExists(const fs::path& relativePath)
{
return fs::exists(m_path / relativePath);
}
private:
fs::path m_path;
};
TEST_CLASS(IsJsonFileCorruptedTests)
{
public:
// Tests IsJsonFileCorrupted: valid JSON with no null bytes returns false.
// Covers: configBackup.h IsJsonFileCorrupted — happy path, full file scan.
TEST_METHOD(CleanJsonFileIsNotCorrupted)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark","startup":true})");
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
}
// Tests IsJsonFileCorrupted: zero-length file returns false (empty is not corrupted).
// Covers: configBackup.h IsJsonFileCorrupted — file.read returns 0 bytes immediately.
TEST_METHOD(EmptyFileIsNotCorrupted)
{
TempDir dir;
dir.WriteFile(L"empty.json", "");
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"empty.json"));
}
// Tests IsJsonFileCorrupted: file containing embedded null bytes returns true.
// Covers: configBackup.h IsJsonFileCorrupted — null byte detection within buffer.
TEST_METHOD(FileWithNullBytesIsCorrupted)
{
TempDir dir;
std::vector<char> corrupted = { '{', '"', 'a', '"', ':', '\0', '\0', '\0', '}' };
dir.WriteFileBytes(L"corrupted.json", corrupted);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"corrupted.json"));
}
// Tests IsJsonFileCorrupted: file entirely filled with 0x00 bytes returns true.
// Reproduces the exact bug from #46179 where installer zeroed out JSON files.
// Covers: configBackup.h IsJsonFileCorrupted — first byte is null.
TEST_METHOD(FileFilledWithNullBytesIsCorrupted)
{
TempDir dir;
std::vector<char> allNulls(1024, '\0');
dir.WriteFileBytes(L"workspaces.json", allNulls);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"workspaces.json"));
}
// Tests IsJsonFileCorrupted: path that does not exist returns false.
// Covers: configBackup.h IsJsonFileCorrupted — file.is_open() check.
TEST_METHOD(NonExistentFileIsNotCorrupted)
{
TempDir dir;
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"missing.json"));
}
// Tests IsJsonFileCorrupted: file larger than the 4096-byte read chunk
// with no null bytes returns false.
// Covers: configBackup.h IsJsonFileCorrupted — multi-chunk while loop.
TEST_METHOD(LargeCleanFileIsNotCorrupted)
{
TempDir dir;
std::string largeContent(8192, 'x');
dir.WriteFile(L"large.json", largeContent);
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"large.json"));
}
// Tests IsJsonFileCorrupted: null byte placed after the first 4096-byte
// chunk boundary is still detected.
// Covers: configBackup.h IsJsonFileCorrupted — second chunk scan.
TEST_METHOD(NullByteAtEndOfLargeFileIsDetected)
{
TempDir dir;
std::string content(5000, 'x');
content[4999] = '\0';
std::vector<char> bytes(content.begin(), content.end());
dir.WriteFileBytes(L"sneaky.json", bytes);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"sneaky.json"));
}
};
TEST_CLASS(BackupConfigFilesTests)
{
public:
// Tests BackupConfigFiles: root-level .json files are copied to ConfigBackup.
// Covers: configBackup.h BackupConfigFiles — root directory_iterator,
// is_regular_file && extension == ".json" branch.
// Setup: Two root-level JSON files.
TEST_METHOD(BackupCopiesRootJsonFiles)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"UpdateState.json", R"({"state":0})");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\UpdateState.json"));
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
}
// Tests BackupConfigFiles: .json files inside module subdirectories are
// copied to ConfigBackup/<module>/.
// Covers: configBackup.h BackupConfigFiles — is_directory branch,
// module directory_iterator with extension filter.
// Setup: Root JSON + two module directories with JSON files.
TEST_METHOD(BackupCopiesModuleJsonFiles)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[]})");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
Assert::AreEqual(std::string(R"({"zones":[]})"),
dir.ReadFile(L"ConfigBackup\\FancyZones\\settings.json"));
}
// Tests BackupConfigFiles: non-.json files at root level are not copied.
// Covers: configBackup.h BackupConfigFiles — extension filter excludes .log.
// Setup: One JSON file + one .log file at root.
TEST_METHOD(BackupSkipsNonJsonFiles)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"debug.log", "log data");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\debug.log"));
}
// Tests BackupConfigFiles: the "Updates" directory is explicitly skipped.
// Covers: configBackup.h BackupConfigFiles — dirName == L"Updates" continue.
// Setup: Root JSON + Updates directory containing a file.
TEST_METHOD(BackupSkipsUpdatesDirectory)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"Updates\\installer.exe", "fake exe");
updating::BackupConfigFiles(dir.path());
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\Updates"));
}
// Tests BackupConfigFiles: running backup twice overwrites the previous
// backup with current file content.
// Covers: configBackup.h BackupConfigFiles — fs::remove_all(backupDir) +
// copy_options::overwrite_existing.
// Setup: Backup, modify original, backup again.
TEST_METHOD(BackupOverwritesPreviousBackup)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"version":1})");
updating::BackupConfigFiles(dir.path());
// Update the original
dir.WriteFile(L"settings.json", R"({"version":2})");
updating::BackupConfigFiles(dir.path());
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
}
// Tests BackupConfigFiles: non-.json files inside module subdirectories
// (e.g., FancyZones/zones.dat) should NOT be backed up.
// Covers: configBackup.h BackupConfigFiles — extension filter in module loop.
TEST_METHOD(BackupSkipsNonJsonFilesInModuleDirs)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({})");
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
dir.WriteFile(L"FancyZones\\zones.dat", "binary data");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\FancyZones\\zones.dat"));
}
// Tests BackupConfigFiles: empty root directory with no files produces
// an empty ConfigBackup dir without errors.
// Covers: configBackup.h BackupConfigFiles — empty directory_iterator.
TEST_METHOD(BackupEmptyRootDirSucceeds)
{
TempDir dir;
// Root dir exists but has no files
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup"));
}
};
TEST_CLASS(RestoreCorruptedConfigsTests)
{
public:
// Tests RestoreCorruptedConfigs: corrupted root-level JSON file is restored
// from the good backup copy.
// Covers: configBackup.h RestoreCorruptedConfigs — root file restore branch,
// fs::exists + IsJsonFileCorrupted + backup integrity check.
// Setup: Good file -> backup -> corrupt original -> restore.
TEST_METHOD(RestoreFixesCorruptedRootFile)
{
TempDir dir;
const std::string goodContent = R"({"theme":"dark"})";
dir.WriteFile(L"settings.json", goodContent);
// Backup
updating::BackupConfigFiles(dir.path());
// Corrupt the original
std::vector<char> corrupted(goodContent.size(), '\0');
dir.WriteFileBytes(L"settings.json", corrupted);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
// Restore
updating::RestoreCorruptedConfigs(dir.path());
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
Assert::AreEqual(goodContent, dir.ReadFile(L"settings.json"));
}
// Tests RestoreCorruptedConfigs: corrupted module-level JSON file is restored
// from the good backup copy.
// Covers: configBackup.h RestoreCorruptedConfigs — module directory branch,
// moduleBackupEntry restore with integrity check.
// Setup: Module file + root file -> backup -> corrupt module file -> restore.
TEST_METHOD(RestoreFixesCorruptedModuleFile)
{
TempDir dir;
const std::string goodContent = R"({"workspaces":[]})";
dir.WriteFile(L"Workspaces\\workspaces.json", goodContent);
dir.WriteFile(L"settings.json", R"({})");
updating::BackupConfigFiles(dir.path());
// Corrupt the module file
std::vector<char> corrupted(goodContent.size(), '\0');
dir.WriteFileBytes(L"Workspaces\\workspaces.json", corrupted);
updating::RestoreCorruptedConfigs(dir.path());
Assert::AreEqual(goodContent, dir.ReadFile(L"Workspaces\\workspaces.json"));
}
// Tests RestoreCorruptedConfigs: clean (non-corrupted) files are NOT
// overwritten by backup — preserves user changes made after backup.
// Covers: configBackup.h RestoreCorruptedConfigs — IsJsonFileCorrupted
// returns false, copy_file is skipped.
// Setup: File -> backup -> modify (but keep valid) -> restore.
TEST_METHOD(RestoreLeavesCleanFilesUntouched)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"version":1})");
updating::BackupConfigFiles(dir.path());
// Modify original (but keep it clean JSON)
dir.WriteFile(L"settings.json", R"({"version":2})");
updating::RestoreCorruptedConfigs(dir.path());
// Should NOT have been restored since it's not corrupted
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"settings.json"));
}
// Tests RestoreCorruptedConfigs: when no ConfigBackup directory exists,
// restore silently does nothing (no crash, no data loss).
// Covers: configBackup.h RestoreCorruptedConfigs — !fs::exists(backupDir)
// early return.
// Setup: File with no prior backup.
TEST_METHOD(RestoreHandlesMissingBackupDirectory)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
// No backup was created - restore should silently do nothing
updating::RestoreCorruptedConfigs(dir.path());
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"settings.json"));
}
// Tests RestoreCorruptedConfigs: end-to-end scenario with multiple modules,
// some corrupted and some clean, verifying selective restore.
// Covers: configBackup.h RestoreCorruptedConfigs — both root and module
// branches, selective restore based on corruption status.
// Setup: 4 modules -> backup -> corrupt 2 -> restore -> verify all 4.
TEST_METHOD(FullBackupAndRestoreRoundTrip)
{
TempDir dir;
// Set up a realistic config structure
dir.WriteFile(L"settings.json", R"({"startup":true,"theme":"dark"})");
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[{"id":1}]})");
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[{"name":"dev"}]})");
dir.WriteFile(L"KeyboardManager\\default.json", R"({"remaps":[]})");
// Backup
updating::BackupConfigFiles(dir.path());
// Corrupt some files (simulating #46179 scenario)
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(100, '\0'));
dir.WriteFileBytes(L"settings.json", std::vector<char>(50, '\0'));
// Leave FancyZones and KBM clean
// Restore
updating::RestoreCorruptedConfigs(dir.path());
// Corrupted files should be restored
Assert::AreEqual(std::string(R"({"startup":true,"theme":"dark"})"), dir.ReadFile(L"settings.json"));
Assert::AreEqual(std::string(R"({"workspaces":[{"name":"dev"}]})"), dir.ReadFile(L"Workspaces\\workspaces.json"));
// Clean files should be unchanged
Assert::AreEqual(std::string(R"({"zones":[{"id":1}]})"), dir.ReadFile(L"FancyZones\\settings.json"));
Assert::AreEqual(std::string(R"({"remaps":[]})"), dir.ReadFile(L"KeyboardManager\\default.json"));
}
// Tests RestoreCorruptedConfigs: when the original file has been deleted
// (not corrupted), restore should NOT recreate it from backup. The installer
// may have intentionally removed obsolete config files.
// Covers: configBackup.h RestoreCorruptedConfigs — fs::exists guard.
TEST_METHOD(RestoreSkipsDeletedOriginals)
{
TempDir dir;
dir.WriteFile(L"obsolete.json", R"({"old":true})");
updating::BackupConfigFiles(dir.path());
// Installer deletes the file
std::error_code ec;
fs::remove(dir.path() / L"obsolete.json", ec);
updating::RestoreCorruptedConfigs(dir.path());
// Should NOT be recreated
Assert::IsFalse(dir.FileExists(L"obsolete.json"));
}
// Tests RestoreCorruptedConfigs: when the backup file itself is corrupted
// (e.g., disk error during backup), restore should NOT copy corrupted
// backup over the original — that would make things worse.
// Covers: configBackup.h RestoreCorruptedConfigs — backup integrity check (B2 fix).
TEST_METHOD(RestoreSkipsCorruptedBackup)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
updating::BackupConfigFiles(dir.path());
// Corrupt BOTH the original AND the backup
std::vector<char> nulls(50, '\0');
dir.WriteFileBytes(L"settings.json", nulls);
dir.WriteFileBytes(L"ConfigBackup\\settings.json", nulls);
updating::RestoreCorruptedConfigs(dir.path());
// Original should still be corrupted — we don't restore from bad backup
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
}
};
// Simulates what actually happens during a PowerToys upgrade:
// 1. User has settings from normal use
// 2. Updater backs up before install (Stage 1)
// 3. Installer runs and corrupts some files (simulated)
// 4. Updater restores corrupted files (Stage 2)
// 5. PT relaunches and finds working configs
TEST_CLASS(UpgradeSimulationTests)
{
public:
// Tests full upgrade simulation: backup -> installer corrupts files -> restore.
// Verifies that corrupted files are restored and clean files are untouched.
// Covers: configBackup.h BackupConfigFiles + RestoreCorruptedConfigs —
// end-to-end with 5 modules, 2 corrupted, 3 clean.
// Setup: Realistic config structure with multiple modules.
TEST_METHOD(SimulateUpgradeWithCorruption)
{
TempDir dir;
// === User's real config state before upgrade ===
dir.WriteFile(L"settings.json",
R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})");
dir.WriteFile(L"FancyZones\\settings.json",
R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})");
dir.WriteFile(L"Workspaces\\workspaces.json",
R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})");
dir.WriteFile(L"KeyboardManager\\default.json",
R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})");
dir.WriteFile(L"MouseWithoutBorders\\settings.json",
R"({"machineKey":"abc123","connectToAll":true})");
// Non-JSON files that should be left alone
dir.WriteFile(L"update.log", "2026-04-11 update started");
// === Stage 1: Backup before killing PT ===
updating::BackupConfigFiles(dir.path());
// Verify backup was created correctly
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\KeyboardManager\\default.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\MouseWithoutBorders\\settings.json"));
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\update.log"));
// === Installer runs: some files get corrupted (the #46179 scenario) ===
// Workspaces JSON filled with null bytes
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(512, '\0'));
// Main settings partially corrupted (null bytes injected)
std::vector<char> partialCorrupt = { '{', '"', 's', '\0', '\0', '\0', '\0', '}' };
dir.WriteFileBytes(L"settings.json", partialCorrupt);
// FancyZones, KBM, and MWB survive the install fine
// (this is realistic - not all files get corrupted)
// === Stage 2: Restore after install completes ===
updating::RestoreCorruptedConfigs(dir.path());
// === Verify: PT relaunches and finds working configs ===
// Corrupted files should be restored from backup
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"Workspaces\\workspaces.json"));
Assert::AreEqual(
std::string(R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})"),
dir.ReadFile(L"settings.json"));
Assert::AreEqual(
std::string(R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})"),
dir.ReadFile(L"Workspaces\\workspaces.json"));
// Clean files should be untouched (not overwritten with backup)
Assert::AreEqual(
std::string(R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})"),
dir.ReadFile(L"FancyZones\\settings.json"));
Assert::AreEqual(
std::string(R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})"),
dir.ReadFile(L"KeyboardManager\\default.json"));
Assert::AreEqual(
std::string(R"({"machineKey":"abc123","connectToAll":true})"),
dir.ReadFile(L"MouseWithoutBorders\\settings.json"));
}
// Tests upgrade from an old version that has fewer modules than the new version.
// Verifies that new module configs (created by the installer) are not touched
// by restore, while corrupted old configs are restored.
// Covers: configBackup.h RestoreCorruptedConfigs — module dir in root that
// has no corresponding backup entry.
// Setup: Old version with 1 module -> backup -> new installer adds module -> corrupt old -> restore.
TEST_METHOD(SimulateUpgradeFromVeryOldVersion)
{
TempDir dir;
// Old version had fewer modules - only settings.json
dir.WriteFile(L"settings.json", R"({"theme":"dark","powertoys_version":"v0.60.0"})");
// Backup
updating::BackupConfigFiles(dir.path());
// New installer creates new module dirs that didn't exist before
dir.WriteFile(L"NewModule\\settings.json", R"({"enabled":true})");
// Old settings get corrupted during upgrade
dir.WriteFileBytes(L"settings.json", std::vector<char>(100, '\0'));
// Restore
updating::RestoreCorruptedConfigs(dir.path());
// Old settings restored
Assert::AreEqual(
std::string(R"({"theme":"dark","powertoys_version":"v0.60.0"})"),
dir.ReadFile(L"settings.json"));
// New module settings untouched (no backup existed for them)
Assert::AreEqual(
std::string(R"({"enabled":true})"),
dir.ReadFile(L"NewModule\\settings.json"));
}
};
// Tests for the update lifecycle: argument passing between Stage 1 and Stage 2,
// relaunch path construction, and the handoff that was broken in #42004/#43011/#44071.
TEST_CLASS(UpdateLifecycleTests)
{
public:
// Tests BuildStage2Arguments: output contains the stage 2 flag, installer path,
// and install directory — all three components needed for Stage 2.
// Covers: updateLifecycle.h BuildStage2Arguments — concatenation logic.
// Setup: Typical paths with spaces (Program Files).
TEST_METHOD(BuildStage2ArgumentsContainsInstallerAndInstallDir)
{
const auto args = updating::BuildStage2Arguments(
L"-update_now_stage_2",
L"C:\\Users\\test\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-x64.exe",
L"C:\\Program Files\\PowerToys");
// Must contain the stage 2 flag
Assert::IsTrue(args.find(L"-update_now_stage_2") != std::wstring::npos);
// Must contain the installer path (quoted)
Assert::IsTrue(args.find(L"powertoyssetup-x64.exe") != std::wstring::npos);
// Must contain the install directory (quoted) — this was MISSING before our fix
Assert::IsTrue(args.find(L"C:\\Program Files\\PowerToys") != std::wstring::npos);
}
// Tests BuildStage2Arguments: both paths are wrapped in double quotes to
// survive CommandLineToArgvW parsing when paths contain spaces.
// Covers: updateLifecycle.h BuildStage2Arguments — quote wrapping.
// Setup: Installer path with spaces.
TEST_METHOD(BuildStage2ArgumentsQuotesBothPaths)
{
const auto args = updating::BuildStage2Arguments(
L"-update_now_stage_2",
L"C:\\path with spaces\\installer.exe",
L"C:\\Program Files\\PowerToys");
// Count quotes — should have 4 (open/close for each path)
size_t quoteCount = std::count(args.begin(), args.end(), L'"');
Assert::AreEqual(size_t{ 4 }, quoteCount);
}
// Tests BuildPowerToysExePath: appends "PowerToys.exe" to the install dir.
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path / operator.
// Setup: Standard install path without trailing backslash.
TEST_METHOD(BuildPowerToysExePathAppendsExeName)
{
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys");
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
}
// Tests BuildPowerToysExePath: trailing backslash does not produce double
// backslash (e.g., "...PowerToys\\PowerToys.exe").
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path normalizes separators.
// Setup: Install path with trailing backslash.
TEST_METHOD(BuildPowerToysExePathHandlesTrailingBackslash)
{
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys\\");
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
}
// Tests BuildPowerToysExePath: empty string produces just "PowerToys.exe".
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path with empty input.
// Setup: Empty install directory string.
TEST_METHOD(BuildPowerToysExePathHandlesEmptyString)
{
const auto path = updating::BuildPowerToysExePath(L"");
Assert::AreEqual(std::wstring(L"PowerToys.exe"), path);
}
// Tests CanRelaunchAfterUpdate: returns true when Stage 2 receives
// the install directory (argCount >= 4), false otherwise.
// This is the gate that prevents relaunch when using an old Stage 1
// that didn't pass the install dir (#42004/#43011/#44071).
// Covers: updateLifecycle.h CanRelaunchAfterUpdate.
TEST_METHOD(CanRelaunchReflectsArgCount)
{
// Old Stage 1 (pre-fix): only passed action + installer = 3 args
Assert::IsFalse(updating::CanRelaunchAfterUpdate(0));
Assert::IsFalse(updating::CanRelaunchAfterUpdate(1));
Assert::IsFalse(updating::CanRelaunchAfterUpdate(2));
Assert::IsFalse(updating::CanRelaunchAfterUpdate(3));
// New Stage 1 (post-fix): passes action + installer + installDir = 4 args
Assert::IsTrue(updating::CanRelaunchAfterUpdate(4));
Assert::IsTrue(updating::CanRelaunchAfterUpdate(5));
}
// Tests BuildStage2Arguments + CommandLineToArgvW round-trip: the exact
// scenario where Stage 1 builds args and Windows parses them in Stage 2.
// Verifies quoting is correct so paths with spaces survive the round trip.
// Covers: updateLifecycle.h BuildStage2Arguments — quote correctness.
// Setup: Realistic paths with spaces and version numbers.
TEST_METHOD(Stage2ArgumentsCanBeRoundTrippedThroughCommandLineToArgvW)
{
const std::wstring installerPath = L"C:\\Users\\test user\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-0.86.0-x64.exe";
const std::wstring installDir = L"C:\\Program Files\\PowerToys";
const auto args = updating::BuildStage2Arguments(L"-update_now_stage_2", installerPath, installDir);
// Simulate what Windows does: prepend a fake exe name and parse
std::wstring commandLine = L"PowerToys.Update.exe " + args;
int argc = 0;
LPWSTR* argv = CommandLineToArgvW(commandLine.c_str(), &argc);
Assert::IsNotNull(argv);
Assert::AreEqual(4, argc);
Assert::AreEqual(std::wstring(L"-update_now_stage_2"), std::wstring(argv[1]));
Assert::AreEqual(installerPath, std::wstring(argv[2]));
Assert::AreEqual(installDir, std::wstring(argv[3]));
LocalFree(argv);
}
};
}

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>UpdatingUnitTests</RootNamespace>
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
<ProjectName>Updating.UnitTests</ProjectName>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseOfMfc>false</UseOfMfc>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\UpdatingUnitTests\</OutDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\;..\..\;..\..\..\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="UpdatingTests.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -1,5 +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.
#include "pch.h"

View File

@@ -1,17 +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.
#ifndef PCH_H
#define PCH_H
#include <atomic>
#include <Windows.h>
// Suppressing 26466 - Don't use static_cast downcasts - in CppUnitTest.h
#pragma warning(push)
#pragma warning(disable : 26466)
#include "CppUnitTest.h"
#pragma warning(pop)
#endif //PCH_H

View File

@@ -1,228 +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.
#pragma once
#include <filesystem>
#include <fstream>
#include <string>
namespace updating
{
namespace fs = std::filesystem;
struct BackupResult
{
int filesBackedUp{ 0 };
int errors{ 0 };
};
struct RestoreResult
{
int filesRestored{ 0 };
int filesChecked{ 0 };
int errors{ 0 };
};
// Check if a JSON file is corrupted (contains null bytes, as seen in #46179)
inline bool IsJsonFileCorrupted(const fs::path& filePath)
{
try
{
std::ifstream file(filePath, std::ios::binary);
if (!file.is_open())
{
return false;
}
constexpr size_t c_readChunkSize{ 4096 };
char buffer[c_readChunkSize];
while (file.read(buffer, c_readChunkSize) || file.gcount() > 0)
{
const auto bytesRead = file.gcount();
for (std::streamsize i = 0; i < bytesRead; ++i)
{
if (buffer[i] == '\0')
{
return true;
}
}
}
return false;
}
catch (...)
{
return true;
}
}
// Backup all JSON config files before update to protect against corruption (#46179)
inline BackupResult BackupConfigFiles(const fs::path& rootPath)
{
BackupResult result{};
try
{
const fs::path backupDir = rootPath / L"ConfigBackup";
std::error_code ec;
fs::remove_all(backupDir, ec);
fs::create_directories(backupDir, ec);
if (ec)
{
result.errors++;
return result;
}
for (const auto& entry : fs::directory_iterator(rootPath, ec))
{
if (ec)
{
result.errors++;
break;
}
if (entry.is_regular_file() && entry.path().extension() == L".json")
{
std::error_code copyEc;
fs::copy_file(entry.path(), backupDir / entry.path().filename(), fs::copy_options::overwrite_existing, copyEc);
if (copyEc)
{
result.errors++;
}
else
{
result.filesBackedUp++;
}
}
else if (entry.is_directory())
{
const auto dirName = entry.path().filename().wstring();
if (dirName == L"ConfigBackup" || dirName == L"Updates")
{
continue;
}
const auto moduleBackup = backupDir / entry.path().filename();
fs::create_directories(moduleBackup, ec);
std::error_code moduleEc;
for (const auto& moduleEntry : fs::directory_iterator(entry.path(), moduleEc))
{
if (moduleEc)
{
result.errors++;
break;
}
if (moduleEntry.is_regular_file() && moduleEntry.path().extension() == L".json")
{
std::error_code copyEc;
fs::copy_file(moduleEntry.path(), moduleBackup / moduleEntry.path().filename(), fs::copy_options::overwrite_existing, copyEc);
if (copyEc)
{
result.errors++;
}
else
{
result.filesBackedUp++;
}
}
}
}
}
}
catch (...)
{
result.errors++;
}
return result;
}
// Restore JSON configs from backup if corruption is detected after update.
// Cleans up the backup directory afterward.
inline RestoreResult RestoreCorruptedConfigs(const fs::path& rootPath)
{
RestoreResult result{};
try
{
const fs::path backupDir = rootPath / L"ConfigBackup";
if (!fs::exists(backupDir))
{
return result;
}
std::error_code ec;
for (const auto& backupEntry : fs::directory_iterator(backupDir, ec))
{
if (ec)
{
result.errors++;
break;
}
if (backupEntry.is_regular_file() && backupEntry.path().extension() == L".json")
{
const auto originalPath = rootPath / backupEntry.path().filename();
result.filesChecked++;
if (fs::exists(originalPath) && IsJsonFileCorrupted(originalPath) && !IsJsonFileCorrupted(backupEntry.path()))
{
std::error_code copyEc;
fs::copy_file(backupEntry.path(), originalPath, fs::copy_options::overwrite_existing, copyEc);
if (copyEc)
{
result.errors++;
}
else
{
result.filesRestored++;
}
}
}
else if (backupEntry.is_directory())
{
const auto moduleDir = rootPath / backupEntry.path().filename();
std::error_code moduleEc;
for (const auto& moduleBackupEntry : fs::directory_iterator(backupEntry.path(), moduleEc))
{
if (moduleEc)
{
result.errors++;
break;
}
if (moduleBackupEntry.is_regular_file() && moduleBackupEntry.path().extension() == L".json")
{
const auto originalModulePath = moduleDir / moduleBackupEntry.path().filename();
result.filesChecked++;
if (fs::exists(originalModulePath) && IsJsonFileCorrupted(originalModulePath) && !IsJsonFileCorrupted(moduleBackupEntry.path()))
{
std::error_code copyEc;
fs::copy_file(moduleBackupEntry.path(), originalModulePath, fs::copy_options::overwrite_existing, copyEc);
if (copyEc)
{
result.errors++;
}
else
{
result.filesRestored++;
}
}
}
}
}
}
// Clean up backup directory after restore check
fs::remove_all(backupDir, ec);
}
catch (...)
{
result.errors++;
}
return result;
}
}

View File

@@ -1,47 +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.
#pragma once
#include <filesystem>
#include <string>
namespace updating
{
namespace fs = std::filesystem;
// Build the command-line arguments for Stage 2.
// Stage 1 passes the installer path and the PT install directory
// so Stage 2 can run the installer and relaunch PowerToys afterward.
// Note: paths containing embedded double-quote characters are not supported.
// This is safe because install paths come from get_module_folderpath().
inline std::wstring BuildStage2Arguments(
const std::wstring& stage2Flag,
const fs::path& installerPath,
const fs::path& installDir)
{
std::wstring arguments{ stage2Flag };
arguments += L" \"";
arguments += installerPath.c_str();
arguments += L"\" \"";
arguments += installDir.c_str();
arguments += L"\"";
return arguments;
}
// Build the full path to PowerToys.exe from the install directory.
// Used by Stage 2 to relaunch PT after a successful update.
inline std::wstring BuildPowerToysExePath(const std::wstring& installDir)
{
return (std::filesystem::path(installDir) / L"PowerToys.exe").wstring();
}
// Determine whether Stage 2 has enough information to relaunch PT.
// Returns true if the install directory argument was provided.
inline bool CanRelaunchAfterUpdate(int argCount)
{
// args[0]=exe, args[1]=action, args[2]=installer, args[3]=installDir
return argCount >= 4;
}
}

View File

@@ -55,7 +55,7 @@
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<RuntimeTypeInfo>true</RuntimeTypeInfo>
<WarningLevel>Level4</WarningLevel>
<PreprocessorDefinitions>WIN32;_WINDOWS;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;SPDLOG_WCHAR_TO_UTF8_SUPPORT;FMT_UNICODE=0;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>WIN32;_WINDOWS;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ObjectFileName>$(IntDir)</ObjectFileName>
<FunctionLevelLinking>true</FunctionLevelLinking>
<EnableParallelCodeGeneration>true</EnableParallelCodeGeneration>
@@ -71,7 +71,7 @@
<ClCompile Include="$(RepoRoot)deps\spdlog\src\file_sinks.cpp" />
<ClCompile Include="$(RepoRoot)deps\spdlog\src\async.cpp" />
<ClCompile Include="$(RepoRoot)deps\spdlog\src\cfg.cpp" />
<ClCompile Include="$(RepoRoot)deps\spdlog\src\bundled_fmtlib_format.cpp" />
<ClCompile Include="$(RepoRoot)deps\spdlog\src\fmt.cpp" />
<ClInclude Include="$(RepoRoot)deps\spdlog\include\spdlog\async.h" />
<ClInclude Include="$(RepoRoot)deps\spdlog\include\spdlog\async_logger-inl.h" />
<ClInclude Include="$(RepoRoot)deps\spdlog\include\spdlog\async_logger.h" />

View File

@@ -698,7 +698,7 @@ void LightSwitchInterface::init_settings()
}
catch (const winrt::hresult_error& e)
{
Logger::error(L"[Light Switch] init_settings: hresult_error 0x{:08X} - {}", static_cast<int32_t>(e.code()), e.message().c_str());
Logger::error(L"[Light Switch] init_settings: hresult_error 0x{:08X} - {}", e.code(), e.message().c_str());
}
catch (const std::exception& e)
{

View File

@@ -58,22 +58,10 @@ namespace ShortcutGuide.Converters
foreach (var key in description.Keys)
{
// Try to parse a string key number to a key code
// Try to parse a string key number to a VirtualKey
if (int.TryParse(key, out int keyCode))
{
switch (keyCode)
{
// 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(keyCode);
break;
default:
shortcutList.Add(Microsoft.PowerToys.Settings.UI.Library.Utilities.Helper.GetKeyName((uint)keyCode));
break;
}
shortcutList.Add(keyCode);
}
else
{

View File

@@ -39,15 +39,20 @@ namespace ShortcutGuide.Helpers
public static ShortcutFile GetShortcutsOfApplication(string applicationName)
{
string path = PathOfManifestFiles;
IEnumerable<string> files = Directory.EnumerateFiles(path, applicationName + ".*.yml") ??
throw new FileNotFoundException($"The file for the application '{applicationName}' was not found in '{path}'.");
string localizedPath = Path.Combine(path, applicationName + $".{Language}.yml");
string fallbackPath = Path.Combine(path, applicationName + ".en-US.yml");
IEnumerable<string> filesEnumerable = files as string[] ?? [.. files];
return filesEnumerable.Any(f => f.EndsWith($".{Language}.yml", StringComparison.InvariantCulture))
? YamlToShortcutList(File.ReadAllText(Path.Combine(path, applicationName + $".{Language}.yml")))
: filesEnumerable.Any(f => f.EndsWith(".en-US.yml", StringComparison.InvariantCulture))
? YamlToShortcutList(File.ReadAllText(filesEnumerable.First(f => f.EndsWith(".en-US.yml", StringComparison.InvariantCulture))))
: throw new FileNotFoundException($"The file for the application '{applicationName}' was not found in '{path}' with the language '{Language}' or 'en-US'.");
if (File.Exists(localizedPath))
{
return YamlToShortcutList(File.ReadAllText(localizedPath));
}
if (File.Exists(fallbackPath))
{
return YamlToShortcutList(File.ReadAllText(fallbackPath));
}
throw new FileNotFoundException($"The file for the application '{applicationName}' was not found in '{path}' with the language '{Language}' or 'en-US'.");
}
/// <summary>
@@ -78,6 +83,36 @@ namespace ShortcutGuide.Helpers
return deserializer.Deserialize<IndexFile>(content);
}
private static readonly object IndexLock = new();
private static IndexFile? cachedIndexFile;
private static DateTime cachedIndexLastWriteTimeUtc;
/// <summary>
/// Retrieves the index YAML file that contains the list of all applications and their shortcuts from the cache.
/// </summary>
/// <returns>A deserialized <see cref="IndexFile"/> object.</returns>
private static IndexFile GetCachedIndexYamlFile()
{
string indexPath = Path.Combine(PathOfManifestFiles, "index.yml");
DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(indexPath);
lock (IndexLock)
{
if (cachedIndexFile is not null && cachedIndexLastWriteTimeUtc == lastWriteTimeUtc)
{
return cachedIndexFile.Value;
}
string content = File.ReadAllText(indexPath);
Deserializer deserializer = new();
cachedIndexFile = deserializer.Deserialize<IndexFile>(content);
cachedIndexLastWriteTimeUtc = lastWriteTimeUtc;
return cachedIndexFile.Value;
}
}
/// <summary>
/// Retrieves all application IDs that should be displayed, based on the foreground window and background processes.
/// </summary>
@@ -93,8 +128,6 @@ namespace ShortcutGuide.Helpers
Dictionary<string, string?> applicationIds = new(StringComparer.Ordinal);
Process[] processes = Process.GetProcesses();
if (NativeMethods.GetWindowThreadProcessId(handle, out uint processId) > 0)
{
string? name = null;
@@ -119,7 +152,7 @@ namespace ShortcutGuide.Helpers
{
try
{
IndexFile.IndexItem match = GetIndexYamlFile().Index.First((s) => !s.BackgroundProcess && IsMatch(name, s.WindowFilter));
IndexFile.IndexItem match = GetCachedIndexYamlFile().Index.First((s) => !s.BackgroundProcess && IsMatch(name, s.WindowFilter));
string? pathForApp = match.WindowFilter == "*" ? null : executablePath;
foreach (var item in match.Apps)
{
@@ -132,48 +165,15 @@ namespace ShortcutGuide.Helpers
}
}
foreach (var item in GetIndexYamlFile().Index.Where((s) => s.BackgroundProcess))
foreach (var item in GetCachedIndexYamlFile().Index.Where((s) => s.BackgroundProcess))
{
try
var foundProcesses = Process.GetProcessesByName(item.WindowFilter);
if (foundProcesses.Length > 0)
{
string? matchedExecutablePath = null;
bool matched = false;
foreach (var p in processes)
foreach (var app in item.Apps)
{
try
{
if (IsMatch(p.MainModule!.ModuleName, item.WindowFilter))
{
matched = true;
if (item.WindowFilter != "*")
{
matchedExecutablePath = p.MainModule!.FileName;
}
break;
}
}
catch (Win32Exception)
{
// Access denied for elevated processes; skip.
}
applicationIds[app] = foundProcesses[0].MainModule?.FileName;
}
if (matched)
{
foreach (var app in item.Apps)
{
// Preserve an existing (foreground) path if one was already set;
// only fill in a path when the slot is currently null.
if (!applicationIds.TryGetValue(app, out string? existing) || existing is null)
{
applicationIds[app] = matchedExecutablePath;
}
}
}
}
catch (InvalidOperationException)
{
}
}
@@ -181,10 +181,17 @@ namespace ShortcutGuide.Helpers
static bool IsMatch(string input, string filter)
{
input = input.ToLower(CultureInfo.InvariantCulture);
filter = filter.ToLower(CultureInfo.InvariantCulture);
string regexPattern = "^" + Regex.Escape(filter).Replace("\\*", ".*") + "$";
return Regex.IsMatch(input, regexPattern);
if (filter == "*")
{
return true;
}
if (filter.ToLowerInvariant().EndsWith(".exe", StringComparison.InvariantCulture))
{
return input == filter[..^4];
}
return false;
}
}
}

View File

@@ -5,6 +5,7 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -32,7 +33,9 @@ namespace ShortcutGuide.Helpers
content = new(PopulateRegex().Replace(content.ToString(), populateStartString + Environment.NewLine));
SettingsUtils settingsUtils = SettingsUtils.Default;
EnabledModules enabledModules = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils).SettingsConfig.Enabled;
SettingsRepository<GeneralSettings> settingsRepository = SettingsRepository<GeneralSettings>.GetInstance(settingsUtils);
settingsRepository.ReloadSettings();
EnabledModules enabledModules = settingsRepository.SettingsConfig.Enabled;
if (enabledModules.AdvancedPaste)
{
AdvancedPasteProperties advancedPasteProperties = SettingsRepository<AdvancedPasteSettings>.GetInstance(settingsUtils).SettingsConfig.Properties;
@@ -57,11 +60,19 @@ namespace ShortcutGuide.Helpers
content.Append(HotkeySettingsToYaml(advancedPasteProperties.AdditionalActions.Transcode.TranscodeToMp3.Shortcut, SettingsResourceLoader.GetString("AdvancedPaste/ModuleTitle"), SettingsResourceLoader.GetString("TranscodeToMp3/Header")));
content.Append(HotkeySettingsToYaml(advancedPasteProperties.AdditionalActions.Transcode.TranscodeToMp4.Shortcut, SettingsResourceLoader.GetString("AdvancedPaste/ModuleTitle"), SettingsResourceLoader.GetString("TranscodeToMp4/Header")));
}
foreach (var action in advancedPasteProperties.CustomActions.Value.Where(a => a.IsShown))
{
content.Append(HotkeySettingsToYaml(action.Shortcut, SettingsResourceLoader.GetString("AdvancedPaste/ModuleTitle"), action.Name));
}
}
if (enabledModules.AlwaysOnTop)
{
content.Append(HotkeySettingsToYaml(SettingsRepository<AlwaysOnTopSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.Hotkey, SettingsResourceLoader.GetString("AlwaysOnTop/ModuleTitle"), SettingsResourceLoader.GetString("AlwaysOnTop_ShortDescription")));
AlwaysOnTopProperties alwaysOnTopProperties = SettingsRepository<AlwaysOnTopSettings>.GetInstance(settingsUtils).SettingsConfig.Properties;
content.Append(HotkeySettingsToYaml(alwaysOnTopProperties.Hotkey, SettingsResourceLoader.GetString("AlwaysOnTop/ModuleTitle"), SettingsResourceLoader.GetString("AlwaysOnTop_ShortDescription")));
content.Append(HotkeySettingsToYaml(alwaysOnTopProperties.IncreaseOpacityHotkey, SettingsResourceLoader.GetString("AlwaysOnTop/ModuleTitle"), SettingsResourceLoader.GetString("AlwaysOnTop_IncreaseOpacityShortcut/Header")));
content.Append(HotkeySettingsToYaml(alwaysOnTopProperties.DecreaseOpacityHotkey, SettingsResourceLoader.GetString("AlwaysOnTop/ModuleTitle"), SettingsResourceLoader.GetString("AlwaysOnTop_DecreaseOpacityShortcut/Header")));
}
if (enabledModules.ColorPicker)
@@ -79,6 +90,7 @@ namespace ShortcutGuide.Helpers
CropAndLockProperties cropAndLockProperties = SettingsRepository<CropAndLockSettings>.GetInstance(settingsUtils).SettingsConfig.Properties;
content.Append(HotkeySettingsToYaml(cropAndLockProperties.ThumbnailHotkey, SettingsResourceLoader.GetString("CropAndLock/ModuleTitle"), SettingsResourceLoader.GetString("CropAndLock_Thumbnail")));
content.Append(HotkeySettingsToYaml(cropAndLockProperties.ReparentHotkey, SettingsResourceLoader.GetString("CropAndLock/ModuleTitle"), SettingsResourceLoader.GetString("CropAndLock_Reparent")));
content.Append(HotkeySettingsToYaml(cropAndLockProperties.ScreenshotHotkey, SettingsResourceLoader.GetString("CropAndLock/ModuleTitle"), SettingsResourceLoader.GetString("CropAndLock_Screenshot")));
}
if (enabledModules.CursorWrap)
@@ -116,6 +128,11 @@ namespace ShortcutGuide.Helpers
content.Append(HotkeySettingsToYaml(SettingsRepository<PeekSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.ActivationShortcut, SettingsResourceLoader.GetString("Peek/ModuleTitle")));
}
if (enabledModules.PowerDisplay)
{
content.Append(HotkeySettingsToYaml(SettingsRepository<PowerDisplaySettings>.GetInstance(settingsUtils).SettingsConfig.Properties.ActivationShortcut, SettingsResourceLoader.GetString("PowerDisplay/ModuleTitle"), SettingsResourceLoader.GetString("Launch_PowerDisplay/Content")));
}
if (enabledModules.PowerLauncher)
{
content.Append(HotkeySettingsToYaml(SettingsRepository<PowerLauncherSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.OpenPowerLauncher, SettingsResourceLoader.GetString("PowerLauncher/ModuleTitle")));
@@ -128,7 +145,7 @@ namespace ShortcutGuide.Helpers
if (enabledModules.ShortcutGuide)
{
content.Append(HotkeySettingsToYaml(SettingsRepository<ShortcutGuideSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.DefaultOpenShortcutGuide, SettingsResourceLoader.GetString("ShortcutGuide/ModuleTitle"), SettingsResourceLoader.GetString("ShortcutGuide_ShortDescription")));
content.Append(HotkeySettingsToYaml(SettingsRepository<ShortcutGuideSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.OpenShortcutGuide, SettingsResourceLoader.GetString("ShortcutGuide/ModuleTitle"), SettingsResourceLoader.GetString("ShortcutGuide_ShortDescription")));
}
if (enabledModules.PowerOcr)
@@ -145,6 +162,8 @@ namespace ShortcutGuide.Helpers
/*
if (enabledModules.ZoomIt)
{
settingsUtils.GetSettings<ZoomItSettings>("ZoomIt")
content.Append(HotkeySettingsToYaml(SettingsRepository<ZoomItSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.ToggleKey, SettingsResourceLoader.GetString("ZoomIt/ModuleTitle"), SettingsResourceLoader.GetString("ZoomIt_ZoomGroup/Header")));
content.Append(HotkeySettingsToYaml(SettingsRepository<ZoomItSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.LiveZoomToggleKey, SettingsResourceLoader.GetString("ZoomIt/ModuleTitle"), SettingsResourceLoader.GetString("ZoomIt_LiveZoomGroup/Header")));
content.Append(HotkeySettingsToYaml(SettingsRepository<ZoomItSettings>.GetInstance(settingsUtils).SettingsConfig.Properties.DrawToggleKey, SettingsResourceLoader.GetString("ZoomIt/ModuleTitle"), SettingsResourceLoader.GetString("ZoomIt_DrawGroup/Header")));
@@ -188,6 +207,11 @@ namespace ShortcutGuide.Helpers
/// <inheritdoc cref="HotkeySettingsToYaml(HotkeySettings, string, string?)"/>
private static string HotkeySettingsToYaml(KeyboardKeysProperty hotkeySettings, string moduleName, string? description = null)
{
if (hotkeySettings is null)
{
return HotkeySettingsToYaml(new HotkeySettings(), moduleName, description);
}
return HotkeySettingsToYaml(hotkeySettings.Value, moduleName, description);
}

View File

@@ -6,6 +6,7 @@ using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Windows;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
@@ -27,8 +28,8 @@ namespace ShortcutGuide
// The module interface passes: <powertoys_pid> [telemetry]
if (args.Length >= 2 && args[1] == "telemetry")
{
Logger.LogInfo("Telemetry mode requested. Sending settings telemetry.");
SendSettingsTelemetry();
// Telemetry-only invocation: send settings telemetry and exit silently.
Logger.LogInfo("Telemetry mode requested. Exiting.");
return;
}
@@ -71,7 +72,8 @@ namespace ShortcutGuide
indexGeneration.WaitForExit();
if (indexGeneration.ExitCode != 0)
{
Logger.LogError($"Index generation failed with exit code {indexGeneration.ExitCode}. There may be a corrupt shortcuts file in \"{ManifestInterpreter.PathOfManifestFiles}\".");
Logger.LogError("Index generation failed with exit code: " + indexGeneration.ExitCode);
MessageBox.Show($"Shortcut Guide encountered an error while generating the index file. There is likely a corrupt shortcuts file in \"{ManifestInterpreter.PathOfManifestFiles}\". Try deleting this directory.", "Error displaying shortcuts", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
@@ -45,6 +46,7 @@ namespace ShortcutGuide
MainWindow.SessionDurationMs,
MainWindow.CloseType));
Current.Exit();
Application.Current.Exit();
};
}

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Common.UI;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -32,8 +33,7 @@ namespace ShortcutGuide
{
public sealed partial class MainWindow : WindowEx
{
private readonly Dictionary<string, string?> _currentApplicationIds;
private readonly Stopwatch _sessionStopwatch = Stopwatch.StartNew();
private Dictionary<string, string?> _currentApplicationIds = [];
private ShortcutFile? _shortcutFile;
private string _selectedAppName = null!;
private string _closeType = "Unknown";
@@ -46,9 +46,17 @@ namespace ShortcutGuide
public MainWindow()
{
this._currentApplicationIds = ManifestInterpreter.GetAllCurrentApplicationIds();
this.InitializeComponent();
Activated += Window_Activated;
Task.Run(async () =>
{
_currentApplicationIds = ManifestInterpreter.GetAllCurrentApplicationIds();
DispatcherQueue.TryEnqueue(() =>
{
this.SetNavItems();
});
});
Title = ResourceLoaderInstance.ResourceLoader.GetString("Title")!;
ExtendsContentIntoTitleBar = true;

View File

@@ -111,10 +111,10 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
switch (res.error())
{
case JsonUtils::WorkspacesFileError::FileReadingError:
formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR)), file);
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), file);
break;
case JsonUtils::WorkspacesFileError::IncorrectFileError:
formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR)), file);
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), file);
break;
}
@@ -137,10 +137,10 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
switch (res.error())
{
case JsonUtils::WorkspacesFileError::FileReadingError:
formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR)), file);
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), file);
break;
case JsonUtils::WorkspacesFileError::IncorrectFileError:
formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR)), file);
formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), file);
break;
}
@@ -151,7 +151,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
if (workspaces.empty())
{
Logger::warn("Workspaces file is empty");
std::wstring formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_EMPTY_FILE)), file);
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_EMPTY_FILE), file);
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}
@@ -169,7 +169,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
if (projectToLaunch.id.empty())
{
Logger::critical(L"Workspace {} not found", cmdArgs.workspaceId);
std::wstring formattedMessage = fmt::format(fmt::runtime(GET_RESOURCE_STRING(IDS_PROJECT_NOT_FOUND)), cmdArgs.workspaceId);
std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_PROJECT_NOT_FOUND), cmdArgs.workspaceId);
MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK);
return 1;
}

View File

@@ -1,9 +1,7 @@
#pragma once
#pragma once
#include "KeyboardListener.g.h"
#include <windows.h>
#include <mutex>
#include <functional>
#include <spdlog/stopwatch.h>
#include <set>

View File

@@ -3,7 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.11.260520004" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3719.77" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />

View File

@@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using System.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -113,13 +112,7 @@ public sealed partial class ExtensionGalleryItemViewModel : ObservableObject
private set => SetProperty(ref field, value);
}
// NOTE: This MUST be ObservableCollection<T> (not IReadOnlyList<T> or List<T>).
// ItemsView.ItemsSource (used on ExtensionGalleryItemPage) goes through a WinRT
// vector adapter; under AOT/trimming, only ObservableCollection<T> has a
// preserved IBindableObservableVector adapter. Other list types raise
// "Argument 'source' is not a supported vector" at set_ItemsSource time,
// crashing the page's first measure pass.
public ObservableCollection<ExtensionGalleryScreenshotViewModel> Screenshots { get; }
public IReadOnlyList<ExtensionGalleryScreenshotViewModel> Screenshots { get; }
public bool HasScreenshots => Screenshots.Count > 0;
@@ -531,14 +524,14 @@ public sealed partial class ExtensionGalleryItemViewModel : ObservableObject
|| uri.Scheme.Equals("ms-appx", StringComparison.OrdinalIgnoreCase);
}
private static ObservableCollection<ExtensionGalleryScreenshotViewModel> BuildScreenshots(List<string>? screenshotUrls)
private static IReadOnlyList<ExtensionGalleryScreenshotViewModel> BuildScreenshots(List<string>? screenshotUrls)
{
ObservableCollection<ExtensionGalleryScreenshotViewModel> screenshots = [];
if (screenshotUrls is null || screenshotUrls.Count == 0)
{
return screenshots;
return [];
}
List<ExtensionGalleryScreenshotViewModel> screenshots = [];
HashSet<string> seenUris = new(OrdinalIgnoreCase);
for (var i = 0; i < screenshotUrls.Count; i++)
{

View File

@@ -49,14 +49,6 @@ public partial class ProviderSettingsViewModel : ObservableObject
public string DisplayName => _provider.DisplayName;
/// <summary>
/// Stable, non-localized identifier from the underlying provider (e.g.
/// "com.microsoft.cmdpal.builtin.calculator"). Exposed for UI tests
/// to target the per-provider <see cref="ProviderSettingsViewModel"/>
/// row via <c>AutomationProperties.AutomationId</c>.
/// </summary>
public string Id => _provider.Id;
public string ExtensionName => _provider.Extension?.ExtensionDisplayName ?? Resources.builtin_extension_name;
public string ExtensionSubtext

View File

@@ -779,48 +779,30 @@ public sealed partial class DockWindow : WindowEx,
private void RequestShowPaletteOnUiThread(Point posDips)
{
// pos is relative to our root. We need to convert to absolute
// virtual-screen coords.
//
// TransformToVisual(null) yields a point in the XamlRoot's coordinate
// space (i.e. the window's client area in DIPs), NOT in screen space.
// To get true screen coordinates we must offset by the window's
// screen-space origin (GetWindowRect, which is in pixels). Without
// this offset, X (for Top/Bottom docks) or Y (for Left/Right docks)
// stays in window-local pixels and the palette ends up on the primary
// monitor when the dock lives on a secondary monitor.
// pos is relative to our root. We need to convert to screen coords.
var rootPosDips = Root.TransformToVisual(null).TransformPoint(new Point(0, 0));
var screenPosDips = new Point(rootPosDips.X + posDips.X, rootPosDips.Y + posDips.Y);
var dpi = PInvoke.GetDpiForWindow(_hwnd);
var scaleFactor = dpi / 96.0;
PInvoke.GetWindowRect(_hwnd, out var ourRect);
var screenPosPixels = new Point(
ourRect.left + (screenPosDips.X * scaleFactor),
ourRect.top + (screenPosDips.Y * scaleFactor));
var screenPosPixels = new Point(screenPosDips.X * scaleFactor, screenPosDips.Y * scaleFactor);
// Use monitor-specific bounds when available
// Note: we compute the quadrant in monitor-local coordinates, but
// keep screenPosPixels in absolute virtual-screen coordinates. Mixing
// the two below (when only one axis is overridden from ourRect, which
// is in virtual-screen coords) produced an off-screen final position
// on secondary monitors.
int screenWidth, screenHeight;
double localX, localY;
if (_targetMonitor is not null)
{
screenWidth = _targetMonitor.Bounds.Width;
screenHeight = _targetMonitor.Bounds.Height;
localX = screenPosPixels.X - _targetMonitor.Bounds.Left;
localY = screenPosPixels.Y - _targetMonitor.Bounds.Top;
// Adjust to monitor-local coordinates for quadrant calculation
screenPosPixels = new Point(
screenPosPixels.X - _targetMonitor.Bounds.Left,
screenPosPixels.Y - _targetMonitor.Bounds.Top);
}
else
{
screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
localX = screenPosPixels.X;
localY = screenPosPixels.Y;
}
// Now we're going to find the best position for the palette.
@@ -838,8 +820,8 @@ public sealed partial class DockWindow : WindowEx,
// On the bottom:
// - anchor to the bottom, left if we're on the left half of the screen
// - anchor to the bottom, right if we're on the right half of the screen
var onTopHalf = localY < screenHeight / 2;
var onLeftHalf = localX < screenWidth / 2;
var onTopHalf = screenPosPixels.Y < screenHeight / 2;
var onLeftHalf = screenPosPixels.X < screenWidth / 2;
var onRightHalf = !onLeftHalf;
var onBottomHalf = !onTopHalf;
@@ -855,6 +837,7 @@ public sealed partial class DockWindow : WindowEx,
// we also need to slide the anchor point a bit away from the dock
var paddingDips = 8;
var paddingPixels = paddingDips * scaleFactor;
PInvoke.GetWindowRect(_hwnd, out var ourRect);
// Depending on the side we're on, we need to offset differently
switch (EffectiveSide)

View File

@@ -350,10 +350,7 @@
Small="{StaticResource IconGridViewItemStyle}" />
<DataTemplate x:Key="ListItemSingleRowViewModelTemplate" x:DataType="viewModels:ListItemViewModel">
<Grid
AutomationProperties.AutomationId="{x:Bind Command.Id, Mode=OneWay}"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
ColumnSpacing="12">
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28" />
<ColumnDefinition Width="*" />
@@ -465,7 +462,6 @@
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AutomationId="{x:Bind Command.Id, Mode=OneWay}"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
BorderThickness="0"
CornerRadius="{StaticResource SmallGridViewItemCornerRadius}"
@@ -490,7 +486,6 @@
Width="{StaticResource MediumGridContainerSize}"
Height="{StaticResource MediumGridContainerSize}"
Padding="8"
AutomationProperties.AutomationId="{x:Bind Command.Id, Mode=OneWay}"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
CornerRadius="{StaticResource MediumGridViewItemCornerRadius}"
ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
@@ -533,7 +528,6 @@
Padding="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AutomationId="{x:Bind Command.Id, Mode=OneWay}"
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
BorderThickness="0"
CornerRadius="{StaticResource GalleryGridViewItemRadius}"

View File

@@ -84,7 +84,6 @@
Name="Command"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
AutomationProperties.AutomationId="{x:Bind Id, Mode=OneWay}"
Click="Command_Click"
Style="{StaticResource SubtleButtonStyle}"
UseSystemFocusVisuals="{StaticResource UseSystemFocusVisuals}">

View File

@@ -46,7 +46,6 @@
x:Uid="Settings_AppearancePage_OpenCommandPaletteButton"
MinWidth="200"
HorizontalContentAlignment="Left"
AutomationProperties.AutomationId="CmdPal_AppearancePage_OpenCommandPalette"
Click="OpenCommandPalette_Click"
Style="{StaticResource SubtleButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
@@ -58,7 +57,6 @@
x:Uid="Settings_AppearancePage_ResetAppearanceButton"
MinWidth="200"
HorizontalContentAlignment="Left"
AutomationProperties.AutomationId="CmdPal_AppearancePage_ResetAppearance"
Command="{x:Bind ViewModel.Appearance.ResetAppearanceSettingsCommand}"
Style="{StaticResource SubtleButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
@@ -70,10 +68,7 @@
</StackPanel>
<controls:SettingsCard x:Uid="Settings_GeneralPage_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE793;}">
<ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_AppearancePage_Theme"
SelectedIndex="{x:Bind ViewModel.Appearance.ThemeIndex, Mode=TwoWay}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.ThemeIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_System_Automation" Tag="Default">
<StackPanel Orientation="Horizontal" Spacing="8">
@@ -103,10 +98,7 @@
x:Uid="Settings_GeneralPage_BackdropStyle_SettingsCard"
HeaderIcon="{ui:FontIcon Glyph=&#xF5EF;}"
IsExpanded="{x:Bind ViewModel.Appearance.IsBackdropOpacityVisible, Mode=OneWay}">
<ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_AppearancePage_BackdropStyle"
SelectedIndex="{x:Bind ViewModel.Appearance.BackdropStyleIndex, Mode=TwoWay}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.BackdropStyleIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Acrylic" />
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Transparent" />
<ComboBoxItem x:Uid="Settings_GeneralPage_BackdropStyle_Mica" />
@@ -134,7 +126,6 @@
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackdropOpacity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackdropOpacityVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_AppearancePage_BackdropOpacity"
Maximum="100"
Minimum="0"
StepFrequency="1"
@@ -152,7 +143,6 @@
<ComboBox
x:Uid="Settings_GeneralPage_ColorizationMode"
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_AppearancePage_ColorizationMode"
SelectedIndex="{x:Bind ViewModel.Appearance.ColorizationModeIndex, Mode=TwoWay}"
Visibility="{x:Bind ViewModel.Appearance.IsBackgroundSettingsEnabled, Mode=OneWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
@@ -212,15 +202,11 @@
x:Uid="Settings_GeneralPage_BackgroundImage_SettingsCard"
Description="{x:Bind ViewModel.Appearance.BackgroundImagePath, Mode=OneWay}"
Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
<Button
x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton"
AutomationProperties.AutomationId="CmdPal_AppearancePage_ChooseBackgroundImage"
Click="PickBackgroundImage_Click" />
<Button x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton" Click="PickBackgroundImage_Click" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImageBrightness_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_AppearancePage_BgImageBrightness"
Maximum="100"
Minimum="-100"
StepFrequency="1"
@@ -229,14 +215,13 @@
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImageBlur_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_AppearancePage_BgImageBlur"
Maximum="50"
Minimum="0"
StepFrequency="1"
Value="{x:Bind ViewModel.Appearance.BackgroundImageBlurAmount, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImageFit_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsBackgroundControlsVisible, Mode=OneWay}">
<ComboBox AutomationProperties.AutomationId="CmdPal_AppearancePage_BgImageFit" SelectedIndex="{x:Bind ViewModel.Appearance.BackgroundImageFitIndex, Mode=TwoWay}">
<ComboBox SelectedIndex="{x:Bind ViewModel.Appearance.BackgroundImageFitIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Fill" />
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Stretch" />
</ComboBox>
@@ -245,7 +230,6 @@
<!-- Background tint color and intensity -->
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTint_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsCustomTintVisible, Mode=OneWay}">
<ptControls:ColorPickerButton
AutomationProperties.AutomationId="CmdPal_AppearancePage_BgTintColor"
HasSelectedColor="True"
IsAlphaEnabled="False"
PaletteColors="{x:Bind ViewModel.Appearance.Swatches}"
@@ -254,7 +238,6 @@
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsColorIntensityVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_AppearancePage_BgTintIntensity"
Maximum="100"
Minimum="1"
StepFrequency="1"
@@ -263,7 +246,6 @@
<controls:SettingsCard x:Uid="Settings_GeneralPage_ImageTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsImageTintIntensityVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_AppearancePage_BgImageTintIntensity"
Maximum="100"
Minimum="0"
StepFrequency="1"
@@ -273,10 +255,7 @@
<!-- Reset appearance properties -->
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.Appearance.IsResetButtonVisible, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton"
AutomationProperties.AutomationId="CmdPal_AppearancePage_ResetBgImage"
Command="{x:Bind ViewModel.Appearance.ResetBackgroundImagePropertiesCommand}" />
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.Appearance.ResetBackgroundImagePropertiesCommand}" />
</StackPanel>
</controls:SettingsCard>
@@ -288,18 +267,15 @@
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="Settings_GeneralPage_ShowAppDetails_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE8A0;}">
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_AppearancePage_ShowAppDetails" IsOn="{x:Bind ViewModel.ShowAppDetails, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowAppDetails, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_BackspaceGoesBack_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE750;}">
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_AppearancePage_BackspaceGoesBack" IsOn="{x:Bind ViewModel.BackspaceGoesBack, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind ViewModel.BackspaceGoesBack, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_EscapeKeyBehavior_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE845;}">
<ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_AppearancePage_EscapeKeyBehavior"
SelectedIndex="{x:Bind ViewModel.EscapeKeyBehaviorIndex, Mode=TwoWay}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.EscapeKeyBehaviorIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_DismissEmptySearchOrGoBack" />
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_AlwaysGoBack" />
<ComboBoxItem x:Uid="Settings_GeneralPage_EscapeKeyBehavior_Option_AlwaysDismiss" />
@@ -308,11 +284,11 @@
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_SingleClickActivation_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE962;}">
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_AppearancePage_SingleClickActivates" IsOn="{x:Bind ViewModel.SingleClickActivates, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind ViewModel.SingleClickActivates, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_DisableAnimations_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE945;}">
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_AppearancePage_DisableAnimations" IsOn="{x:Bind ViewModel.DisableAnimations, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind ViewModel.DisableAnimations, Mode=TwoWay}" />
</controls:SettingsCard>
</StackPanel>
</Grid>

View File

@@ -41,12 +41,11 @@
x:Uid="CmdPalDock_LearnMore"
Margin="0,0,0,36"
Padding="0"
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_LearnMore"
FontWeight="SemiBold"
NavigateUri="https://aka.ms/cmdpal-dock" />
<!-- Enable Dock -->
<controls:SettingsCard x:Uid="Settings_GeneralPage_EnableDock_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xF596;}">
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_DockSettingsPage_EnableDock" IsOn="{x:Bind ViewModel.EnableDock, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind ViewModel.EnableDock, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Appearance Section -->
@@ -83,10 +82,7 @@
</controls:SettingsCard>
<controls:SettingsCard x:Uid="DockAppearance_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE793;}">
<ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_Theme"
SelectedIndex="{x:Bind ViewModel.DockAppearance.ThemeIndex, Mode=TwoWay}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ThemeIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_System_Automation" Tag="Default">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE770;" />
@@ -127,10 +123,7 @@
x:Uid="DockAppearance_Background_SettingsExpander"
HeaderIcon="{ui:FontIcon Glyph=&#xE790;}"
IsExpanded="{x:Bind ViewModel.DockAppearance.IsColorizationDetailsExpanded, Mode=TwoWay}">
<ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_ColorizationMode"
SelectedIndex="{x:Bind ViewModel.DockAppearance.ColorizationModeIndex, Mode=TwoWay}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ColorizationModeIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
@@ -183,15 +176,11 @@
x:Uid="DockAppearance_BackgroundImage_SettingsCard"
Description="{x:Bind ViewModel.DockAppearance.BackgroundImagePath, Mode=OneWay}"
Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
<Button
x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton"
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_ChooseBackgroundImage"
Click="PickBackgroundImage_Click" />
<Button x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton" Click="PickBackgroundImage_Click" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBrightness_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_BgImageBrightness"
Maximum="100"
Minimum="-100"
StepFrequency="1"
@@ -200,14 +189,13 @@
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBlur_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_BgImageBlur"
Maximum="50"
Minimum="0"
StepFrequency="1"
Value="{x:Bind ViewModel.DockAppearance.BackgroundImageBlurAmount, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageFit_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
<ComboBox AutomationProperties.AutomationId="CmdPal_DockSettingsPage_BgImageFit" SelectedIndex="{x:Bind ViewModel.DockAppearance.BackgroundImageFitIndex, Mode=TwoWay}">
<ComboBox SelectedIndex="{x:Bind ViewModel.DockAppearance.BackgroundImageFitIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Fill" />
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Stretch" />
</ComboBox>
@@ -224,7 +212,6 @@
<controls:SettingsCard x:Uid="DockAppearance_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsCustomTintIntensityVisible, Mode=OneWay}">
<Slider
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_BgTintIntensity"
Maximum="100"
Minimum="1"
StepFrequency="1"
@@ -233,10 +220,7 @@
<!-- Reset background image properties -->
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
<Button
x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton"
AutomationProperties.AutomationId="CmdPal_DockSettingsPage_ResetBgImage"
Command="{x:Bind ViewModel.DockAppearance.ResetBackgroundImagePropertiesCommand}" />
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.DockAppearance.ResetBackgroundImagePropertiesCommand}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
@@ -246,7 +230,7 @@
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="DockBehavior_AlwaysOnTop_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE840;}">
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_DockSettingsPage_AlwaysOnTop" IsOn="{x:Bind ViewModel.Dock_AlwaysOnTop, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind ViewModel.Dock_AlwaysOnTop, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- Monitors Section -->
@@ -259,17 +243,10 @@
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="dockVm:DockMonitorConfigViewModel">
<controls:SettingsExpander
AutomationProperties.AutomationId="{x:Bind DeviceId, Mode=OneWay}"
Description="{x:Bind Resolution, Mode=OneWay}"
Header="{x:Bind DisplayName, Mode=OneWay}"
HeaderIcon="{ui:FontIcon Glyph=&#xE7F4;}"
IsExpanded="False">
<!--
AutomationId on the inner ToggleSwitch / ComboBox is intentionally
omitted because the parent SettingsExpander already exposes a
per-monitor unique AutomationId (DeviceId), so tests can locate
each control relative to its monitor without ambiguity.
-->
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="DockMonitor_Position_SettingsCard">

View File

@@ -38,7 +38,6 @@
<Button
Margin="0,12,0,0"
HorizontalAlignment="Stretch"
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_InstallViaWinGet"
Command="{x:Bind ViewModel.InstallViaWinGetCommand, Mode=OneWay}"
Content="{x:Bind ViewModel.InstallViaWinGetText, Mode=OneWay}"
IsEnabled="{x:Bind ViewModel.CanInstallViaWinGet, Mode=OneWay}"
@@ -49,7 +48,6 @@
Width="32"
Height="32"
Padding="0"
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_CancelWinGetAction"
Command="{x:Bind ViewModel.CancelWinGetActionCommand, Mode=OneWay}"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind help:BindTransformers.BoolToVisibility(ViewModel.ShowCancelWinGetActionButton), Mode=OneWay, FallbackValue=Collapsed}">
@@ -85,7 +83,6 @@
Height="28"
Padding="0"
VerticalAlignment="Center"
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_CopyInstallCommand"
Command="{x:Bind ViewModel.CopyWinGetInstallCommand, Mode=OneWay}"
IsEnabled="{x:Bind ViewModel.CanCopyWinGetInstallCommand, Mode=OneWay}"
Style="{StaticResource SubtleButtonStyle}">
@@ -219,7 +216,6 @@
<HyperlinkButton
x:Uid="Settings_GalleryItemPage_AuthorLink"
Padding="0"
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_AuthorLink"
Command="{x:Bind ViewModel.OpenAuthorPageCommand, Mode=OneWay}"
Visibility="{x:Bind help:BindTransformers.BoolToVisibility(ViewModel.HasAuthorUrl), Mode=OneWay, FallbackValue=Collapsed}">
<StackPanel Orientation="Horizontal" Spacing="4">
@@ -231,7 +227,6 @@
<HyperlinkButton
Grid.Row="3"
Padding="0"
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_ViewRepository"
NavigateUri="{x:Bind ViewModel.Homepage, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ViewModel.Homepage, Mode=OneWay}"
Visibility="{x:Bind help:BindTransformers.BoolToVisibility(ViewModel.HasHomepage), Mode=OneWay, FallbackValue=Collapsed}">
@@ -315,7 +310,6 @@
x:Uid="Settings_GalleryItemPage_Uninstall_Link"
Padding="0"
VerticalAlignment="Center"
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_OpenInstalledApps"
Command="{x:Bind ViewModel.OpenInstalledAppsCommand}"
FontSize="12"
Visibility="{x:Bind help:BindTransformers.BoolToVisibility(ViewModel.ShowInstalledBadge), Mode=OneWay, FallbackValue=Collapsed}" />

View File

@@ -71,7 +71,7 @@
<Run Text="{x:Bind ViewModel.DisplayName}" />
</TextBlock>
</controls:SettingsCard.Header>
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_ExtensionPage_Enable" IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SwitchPresenter
HorizontalAlignment="Stretch"
@@ -109,15 +109,11 @@
<TextBox
x:Uid="Settings_ExtensionPage_Alias_PlaceholderText"
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_ExtensionPage_AliasText"
AutomationProperties.LabeledBy="{x:Bind AliasSettingsCard}"
Text="{x:Bind AliasText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_ExtensionPage_AliasActivation_SettingsCard" IsEnabled="{x:Bind AliasText, Converter={StaticResource StringEmptyToBoolConverter}, Mode=OneWay}">
<ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_ExtensionPage_AliasActivationType"
SelectedIndex="{x:Bind IsDirectAlias, Converter={StaticResource BoolToOptionConverter}, Mode=TwoWay}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind IsDirectAlias, Converter={StaticResource BoolToOptionConverter}, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_ExtensionPage_Alias_DirectComboBox" />
<ComboBoxItem x:Uid="Settings_ExtensionPage_Alias_IndirectComboBox" />
</ComboBox>
@@ -140,7 +136,6 @@
Margin="0,0,0,4"
Padding="0"
VerticalAlignment="Bottom"
AutomationProperties.AutomationId="CmdPal_ExtensionPage_ManageFallbackRank"
Click="RankButton_Click">
<HyperlinkButton.Content>
<StackPanel Orientation="Horizontal" Spacing="8">
@@ -162,7 +157,6 @@
<DataTemplate x:DataType="viewModels:FallbackSettingsViewModel">
<controls:SettingsExpander
Grid.Column="1"
AutomationProperties.AutomationId="{x:Bind Id, Mode=OneWay}"
Header="{x:Bind DisplayName, Mode=OneWay}"
IsExpanded="False">
<controls:SettingsExpander.HeaderIcon>
@@ -178,13 +172,6 @@
</cpcontrols:ContentIcon>
</controls:SettingsExpander.HeaderIcon>
<!--
AutomationId on the inner ToggleSwitch is intentionally
omitted because the parent SettingsExpander already
exposes a per-fallback unique AutomationId (Id), so
tests can locate the toggle relative to that without
ambiguity across rows.
-->
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>

View File

@@ -92,7 +92,6 @@
<HyperlinkButton
x:Uid="Settings_ExtensionsPage_Banner_Hyperlink"
Padding="0"
AutomationProperties.AutomationId="CmdPal_ExtensionsPage_LearnMore"
NavigateUri="https://aka.ms/building-cmdpal-extensions" />
</StackPanel>
<StackPanel
@@ -100,10 +99,7 @@
Grid.Column="1"
Orientation="Horizontal"
Spacing="8">
<Button
x:Uid="Settings_ExtensionsPage_FindExtensions_MicrosoftStore"
AutomationProperties.AutomationId="CmdPal_ExtensionsPage_OpenMicrosoftStore"
Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}">
<Button x:Uid="Settings_ExtensionsPage_FindExtensions_MicrosoftStore" Command="{x:Bind viewModel.Extensions.OpenStoreWithExtensionCommand}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16">
<Image AutomationProperties.AccessibilityView="Raw" Source="{ThemeResource StoreLogo}" />
@@ -216,7 +212,6 @@
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="viewModels:ProviderSettingsViewModel">
<controls:SettingsCard
AutomationProperties.AutomationId="{x:Bind Id, Mode=OneWay}"
Click="SettingsCard_Click"
DataContext="{x:Bind}"
Description="{x:Bind ExtensionSubtext, Mode=OneWay}"
@@ -248,12 +243,6 @@
</cpcontrols:ContentIcon.Content>
</cpcontrols:ContentIcon>
</controls:SettingsCard.HeaderIcon>
<!--
AutomationId on the inner ToggleSwitch is intentionally omitted
because the parent SettingsCard already exposes a unique
provider-derived AutomationId (Provider.Id), so tests can locate
the toggle relative to that without ambiguity across rows.
-->
<ToggleSwitch IsOn="{x:Bind IsEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
</DataTemplate>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Microsoft.CmdPal.UI.Settings.GeneralPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
@@ -39,41 +39,27 @@
<HyperlinkButton
x:Uid="CmdPal_LearnMore"
Padding="0"
AutomationProperties.AutomationId="CmdPal_GeneralPage_LearnMore"
FontWeight="SemiBold"
NavigateUri="https://aka.ms/cmdpal" />
<TextBlock x:Uid="ActivationSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsExpander
x:Uid="Settings_GeneralPage_ActivationKey_SettingsExpander"
AutomationProperties.AutomationId="CmdPal_GeneralPage_ActivationKey"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"
IsExpanded="True">
<ptControls:ShortcutControl AutomationProperties.AutomationId="CmdPal_GeneralPage_HotkeyShortcut" HotkeySettings="{x:Bind viewModel.Hotkey, Mode=TwoWay}" />
<ptControls:ShortcutControl HotkeySettings="{x:Bind viewModel.Hotkey, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard ContentAlignment="Left">
<ptcontrols:CheckBoxWithDescriptionControl
x:Uid="Settings_GeneralPage_LowLevelHook_SettingsCard"
AutomationProperties.AutomationId="CmdPal_GeneralPage_LowLevelHook"
IsChecked="{x:Bind viewModel.UseLowLevelGlobalHotkey, Mode=TwoWay}" />
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="Settings_GeneralPage_LowLevelHook_SettingsCard" IsChecked="{x:Bind viewModel.UseLowLevelGlobalHotkey, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard ContentAlignment="Left">
<ptcontrols:CheckBoxWithDescriptionControl
x:Uid="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard"
AutomationProperties.AutomationId="CmdPal_GeneralPage_IgnoreShortcutWhenFullscreen"
IsChecked="{x:Bind viewModel.IgnoreShortcutWhenFullscreen, Mode=TwoWay}" />
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard" IsChecked="{x:Bind viewModel.IgnoreShortcutWhenFullscreen, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard ContentAlignment="Left">
<ptcontrols:CheckBoxWithDescriptionControl
x:Uid="Settings_GeneralPage_IgnoreShortcutWhenBusy_SettingsCard"
AutomationProperties.AutomationId="CmdPal_GeneralPage_IgnoreShortcutWhenBusy"
IsChecked="{x:Bind viewModel.IgnoreShortcutWhenBusy, Mode=TwoWay}" />
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="Settings_GeneralPage_IgnoreShortcutWhenBusy_SettingsCard" IsChecked="{x:Bind viewModel.IgnoreShortcutWhenBusy, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard ContentAlignment="Left">
<ptcontrols:CheckBoxWithDescriptionControl
x:Uid="Settings_GeneralPage_AllowBreakthroughShortcut_SettingsCard"
AutomationProperties.AutomationId="CmdPal_GeneralPage_AllowBreakthroughShortcut"
IsChecked="{x:Bind viewModel.AllowBreakthroughShortcut, Mode=TwoWay}" />
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="Settings_GeneralPage_AllowBreakthroughShortcut_SettingsCard" IsChecked="{x:Bind viewModel.AllowBreakthroughShortcut, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
<controls:SettingsExpander.ItemsFooter>
@@ -85,10 +71,7 @@
</controls:SettingsExpander.ItemsFooter>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="Settings_GeneralPage_AutoGoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE80F;}">
<ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_GeneralPage_AutoGoHome"
SelectedIndex="{x:Bind viewModel.AutoGoBackIntervalIndex, Mode=TwoWay}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind viewModel.AutoGoBackIntervalIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Never" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Immediately" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After10Seconds" />
@@ -101,16 +84,13 @@
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_HighlightSearch_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE933;}">
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_GeneralPage_HighlightSearch" IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_KeepPreviousQuery_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE81C;}">
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_GeneralPage_KeepPreviousQuery" IsOn="{x:Bind viewModel.KeepPreviousQuery, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind viewModel.KeepPreviousQuery, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Run_PositionHeader" HeaderIcon="{ui:FontIcon Glyph=&#xe78b;}">
<ComboBox
MinWidth="{StaticResource SettingActionControlMinWidth}"
AutomationProperties.AutomationId="CmdPal_GeneralPage_MonitorPosition"
SelectedIndex="{x:Bind viewModel.MonitorPositionIndex, Mode=TwoWay}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind viewModel.MonitorPositionIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Run_Radio_Position_Cursor" />
<ComboBoxItem x:Uid="Run_Radio_Position_Primary_Monitor" />
<ComboBoxItem x:Uid="Run_Radio_Position_Focus" />
@@ -124,7 +104,7 @@
<TextBlock x:Uid="BehaviorSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE75B;}">
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_GeneralPage_ShowSystemTrayIcon" IsOn="{x:Bind viewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind viewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- 'For Developers' section -->
@@ -132,17 +112,14 @@
<TextBlock x:Uid="ForDevelopersSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="Settings_GeneralPage_AllowExternalReload_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE777;}">
<ToggleSwitch AutomationProperties.AutomationId="CmdPal_GeneralPage_AllowExternalReload" IsOn="{x:Bind viewModel.AllowExternalReload, Mode=TwoWay}" />
<ToggleSwitch IsOn="{x:Bind viewModel.AllowExternalReload, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- 'About' section -->
<TextBlock x:Uid="AboutSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsExpander
x:Uid="Settings_GeneralPage_About_SettingsExpander"
AutomationProperties.AutomationId="CmdPal_GeneralPage_About"
HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/StoreLogo.png}">
<controls:SettingsExpander x:Uid="Settings_GeneralPage_About_SettingsExpander" HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/StoreLogo.png}">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
IsTextSelectionEnabled="True"
@@ -150,14 +127,8 @@
<controls:SettingsExpander.Items>
<controls:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Margin="-12,0,0,0" Orientation="Vertical">
<HyperlinkButton
x:Uid="Settings_GeneralPage_About_GithubLink_Hyperlink"
AutomationProperties.AutomationId="CmdPal_GeneralPage_GithubLink"
NavigateUri="https://go.microsoft.com/fwlink/?linkid=2310837" />
<HyperlinkButton
x:Uid="Settings_GeneralPage_About_SDKDocs_Hyperlink"
AutomationProperties.AutomationId="CmdPal_GeneralPage_SDKDocs"
NavigateUri="https://aka.ms/cmdpalextensions-devdocs" />
<HyperlinkButton x:Uid="Settings_GeneralPage_About_GithubLink_Hyperlink" NavigateUri="https://go.microsoft.com/fwlink/?linkid=2310837" />
<HyperlinkButton x:Uid="Settings_GeneralPage_About_SDKDocs_Hyperlink" NavigateUri="https://aka.ms/cmdpalextensions-devdocs" />
</StackPanel>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
@@ -165,7 +136,6 @@
<HyperlinkButton
x:Uid="Settings_GeneralPage_SendFeedback_Hyperlink"
Margin="0,8,0,0"
AutomationProperties.AutomationId="CmdPal_GeneralPage_SendFeedback"
NavigateUri="https://go.microsoft.com/fwlink/?linkid=2310638" />
</StackPanel>
</Grid>

View File

@@ -32,22 +32,13 @@
IsExpanded="True">
<controls:SettingsExpander.Items>
<controls:SettingsCard Header="Throw an unhandled exception from the UI thread">
<Button
AutomationProperties.AutomationId="CmdPal_InternalPage_ThrowMainThreadException"
Click="ThrowPlainMainThreadException_Click"
Content="Throw" />
<Button Click="ThrowPlainMainThreadException_Click" Content="Throw" />
</controls:SettingsCard>
<controls:SettingsCard Header="Throw an unhandled exception from the UI thread (with PII)">
<Button
AutomationProperties.AutomationId="CmdPal_InternalPage_ThrowMainThreadExceptionPii"
Click="ThrowPlainMainThreadExceptionPii_Click"
Content="Throw" />
<Button Click="ThrowPlainMainThreadExceptionPii_Click" Content="Throw" />
</controls:SettingsCard>
<controls:SettingsCard Description="Throw with delay, when the task is collected by the GC" Header="Throw unobserved exception from a task">
<Button
AutomationProperties.AutomationId="CmdPal_InternalPage_ThrowUnobservedTaskException"
Click="ThrowExceptionInUnobservedTask_Click"
Content="Throw" />
<Button Click="ThrowExceptionInUnobservedTask_Click" Content="Throw" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
@@ -58,29 +49,20 @@
x:Name="LogsSettingsCard"
Header="Logs folder"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}">
<Button
AutomationProperties.AutomationId="CmdPal_InternalPage_OpenLogsFolder"
Click="OpenLogsCardClicked"
Content="Open folder" />
<Button Click="OpenLogsCardClicked" Content="Open folder" />
</controls:SettingsCard>
<controls:SettingsCard
x:Name="CurrentLogFileSettingsCard"
Header="Current log file"
HeaderIcon="{ui:FontIcon Glyph=&#xF7BB;}">
<Button
AutomationProperties.AutomationId="CmdPal_InternalPage_OpenCurrentLog"
Click="OpenCurrentLogCardClicked"
Content="Open log" />
<Button Click="OpenCurrentLogCardClicked" Content="Open log" />
</controls:SettingsCard>
<controls:SettingsCard
x:Name="ToggleDevRibbonVisibilitySettingsCard"
Description="This is only temporary and state is not saved"
Header="Toggle dev ribbon visibility"
HeaderIcon="{ui:FontIcon Glyph=&#xE8EC;}">
<Button
AutomationProperties.AutomationId="CmdPal_InternalPage_ToggleDevRibbon"
Click="ToggleDevRibbonClicked"
Content="Toggle dev ribbon" />
<Button Click="ToggleDevRibbonClicked" Content="Toggle dev ribbon" />
</controls:SettingsCard>
<!-- Gallery Section -->
@@ -103,10 +85,7 @@
x:Name="ConfigurationFolderSettingsCard"
Header="Configuration folder"
HeaderIcon="{ui:FontIcon Glyph=&#xF73D;}">
<Button
AutomationProperties.AutomationId="CmdPal_InternalPage_OpenConfigFolder"
Click="OpenConfigFolderCardClick"
Content="Open folder" />
<Button Click="OpenConfigFolderCardClick" Content="Open folder" />
</controls:SettingsCard>

View File

@@ -1,302 +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.VisualStudio.TestTools.UnitTesting;
namespace PowerAccent.Common.UnitTests;
[TestClass]
public sealed class CharacterMappingsTests
{
// Every Language enum value must appear in All exactly once. If a value is missing,
// GetCharacters will silently return no characters for that language. If it appears
// more than once, the second entry is dead code.
[TestMethod]
public void All_ContainsEveryLanguageEnumValue_ExactlyOnce()
{
foreach (Language lang in Enum.GetValues<Language>())
{
var count = CharacterMappings.All.Count(e => e.Id == lang);
Assert.AreEqual(
1,
count,
$"Language.{lang} appears {count} time(s) in CharacterMappings.All — expected exactly 1.");
}
}
/// <summary>
/// The Characters dictionary for each entry in All must not contain null or empty
/// mappings.
/// </summary>
[TestMethod]
public void All_Characters_ContainsNoNullOrEmptyEntries()
{
foreach (var entry in CharacterMappings.All)
{
foreach (var kvp in entry.Characters)
{
var key = kvp.Key;
var mappings = kvp.Value;
Assert.IsNotNull(
mappings,
$"Language.{entry.Id} has a null mappings array for key {key}.");
Assert.IsTrue(
mappings.Length > 0,
$"Language.{entry.Id} has an empty mappings array for key {key}.");
Assert.IsFalse(
mappings.Any(c => string.IsNullOrEmpty(c)),
$"Language.{entry.Id} has null or empty string(s) in its mappings array for key {key}.");
}
}
}
// Every Language enum value must appear in DisplayOrder exactly once. If a value is
// missing, its characters will be silently omitted from the popup. If it appears more
// than once, Collect will emit its characters twice (before Distinct removes them).
[TestMethod]
public void DisplayOrder_ContainsEveryLanguageEnumValue_ExactlyOnce()
{
foreach (Language lang in Enum.GetValues<Language>())
{
var count = CharacterMappings.DisplayOrder.Count(l => l == lang);
Assert.AreEqual(
1,
count,
$"Language.{lang} appears {count} time(s) in CharacterMappings.DisplayOrder - expected exactly 1.");
}
}
// Every LanguageGroup enum value must appear in GroupDisplayOrder exactly once.
[TestMethod]
public void GroupDisplayOrder_ContainsEveryLanguageGroupValue_ExactlyOnce()
{
foreach (LanguageGroup group in Enum.GetValues<LanguageGroup>())
{
var count = CharacterMappings.GroupDisplayOrder.Count(g => g == group);
Assert.AreEqual(
1,
count,
$"LanguageGroup.{group} appears {count} time(s) in CharacterMappings.GroupDisplayOrder - expected exactly 1.");
}
}
// LanguageLookup must contain an entry for every Language enum value, derived from All.
[TestMethod]
public void LanguageLookup_ContainsEveryLanguageEnumValue()
{
foreach (Language lang in Enum.GetValues<Language>())
{
Assert.IsTrue(
CharacterMappings.LanguageLookup.ContainsKey(lang),
$"Language.{lang} is missing from CharacterMappings.LanguageLookup.");
}
}
// Every entry in All must have a non-empty Identifier. A blank identifier would
// produce a malformed resource key (e.g. "QuickAccent_SelectedLanguage_") that
// silently resolves to an empty string in the Settings UI.
[TestMethod]
public void All_EveryEntry_HasNonEmptyIdentifier()
{
foreach (var entry in CharacterMappings.All)
{
Assert.IsFalse(
string.IsNullOrWhiteSpace(entry.Identifier),
$"Language.{entry.Id} has a null or whitespace Identifier.");
}
}
// Every entry in All must have a non-null Characters dictionary. A null would throw
// at runtime inside GetCharacters.
[TestMethod]
public void All_EveryEntry_HasNonNullCharacters()
{
foreach (var entry in CharacterMappings.All)
{
Assert.IsNotNull(
entry.Characters,
$"Language.{entry.Id} has a null Characters dictionary.");
}
}
// Every LanguageGroup enum value must be used by at least one entry in All. This
// guards against a new group being added to the enum but forgotten in the data, which
// would make it impossible to test or exercise that group path.
[TestMethod]
public void All_EveryLanguageGroupValue_IsUsedAtLeastOnce()
{
var usedGroups = CharacterMappings.All.Select(e => e.Group).ToHashSet();
foreach (LanguageGroup group in Enum.GetValues<LanguageGroup>())
{
// UserDefined is reserved for future use and may not yet be populated.
if (group == LanguageGroup.UserDefined)
{
continue;
}
Assert.IsTrue(
usedGroups.Contains(group),
$"LanguageGroup.{group} is defined in the enum but no entry in CharacterMappings.All uses it.");
}
}
// GetCharacters with an empty language array must return an empty array without
// throwing.
[TestMethod]
public void GetCharacters_EmptyLanguages_ReturnsEmpty()
{
var result = CharacterMappings.GetCharacters(LetterKey.VK_A, []);
Assert.AreEqual(0, result.Length);
}
// GetCharacters with all languages must return a non-empty result for a key that is
// mapped in at least one language (VK_A is mapped in the majority of languages).
[TestMethod]
public void GetCharacters_AllLanguages_ReturnsNonEmptyForCommonKey()
{
var allLangs = Enum.GetValues<Language>();
var result = CharacterMappings.GetCharacters(LetterKey.VK_A, allLangs);
Assert.IsTrue(result.Length > 0, "Expected at least one character for VK_A across all languages.");
}
// GetCharacters must deduplicate characters that appear in multiple languages.
// If two languages both map VK_A to the same character, it should appear only once.
[TestMethod]
public void GetCharacters_DeduplicatesCharactersAcrossLanguages()
{
var allLangs = Enum.GetValues<Language>();
var result = CharacterMappings.GetCharacters(LetterKey.VK_A, allLangs);
var distinct = result.Distinct().ToArray();
CollectionAssert.AreEquivalent(
distinct,
result,
"GetCharacters returned duplicate characters. Results should be deduplicated.");
}
// Calling GetCharacters twice with all languages should return the same results,
// confirming the cache path is consistent.
[TestMethod]
public void GetCharacters_AllLanguagesCachedResult_IsConsistent()
{
var allLangs = Enum.GetValues<Language>();
var first = CharacterMappings.GetCharacters(LetterKey.VK_E, allLangs);
var second = CharacterMappings.GetCharacters(LetterKey.VK_E, allLangs);
CollectionAssert.AreEqual(first, second, "Cached and non-cached results for VK_E differ.");
}
// GetCharacters for a single language should return exactly that language's
// characters for a key it maps. The test derives both the language and key from the
// live data so it stays valid regardless of future mapping changes.
[TestMethod]
public void GetCharacters_SingleLanguage_ReturnsOnlyThatLanguagesCharacters()
{
var langInfo = CharacterMappings.All.First(l => l.Characters.Count > 0);
var (key, expected) = langInfo.Characters.First();
var result = CharacterMappings.GetCharacters(key, [langInfo.Id]);
CollectionAssert.AreEquivalent(
expected,
result,
$"GetCharacters for Language.{langInfo.Id} / LetterKey.{key} did not match the mapped characters.");
}
// GetCharacters must throw KeyNotFoundException when passed a Language value that is
// not in LanguageLookup (i.e. not in All). This is deliberate fail-fast behaviour:
// an unknown language is a programming error, not a recoverable condition. The cast
// produces a valid enum value that was never registered in All.
[TestMethod]
public void GetCharacters_UnknownLanguage_ThrowsKeyNotFoundException()
{
var unknown = (Language)(-1);
Assert.ThrowsExactly<KeyNotFoundException>(
() => CharacterMappings.GetCharacters(LetterKey.VK_A, [unknown]),
"Expected KeyNotFoundException when a Language value absent from LanguageLookup is passed to GetCharacters.");
}
/// <summary>
/// GetCharacters must return characters sorted strictly by GroupDisplayOrder and
/// then DisplayOrder, regardless of the sequence of languages passed in.
/// </summary>
[TestMethod]
public void GetCharacters_SortsOutput_AccordingToDisplayOrder()
{
// Input in the wrong order.
Language[] input = [Language.SPECIAL, Language.PI, Language.FR];
var result = CharacterMappings.GetCharacters(LetterKey.VK_A, input);
// Derive correct order.
var expectedOrder = CharacterMappings.All
.Where(lang => input.Contains(lang.Id))
.OrderBy(lang => CharacterMappings.GroupDisplayOrder.ToList().IndexOf(lang.Group))
.ThenBy(lang => CharacterMappings.DisplayOrder.ToList().IndexOf(lang.Id))
.SelectMany(lang => lang.Characters.TryGetValue(LetterKey.VK_A, out var chars) ? chars : [])
.Distinct()
.ToArray();
CollectionAssert.AreEqual(
expectedOrder,
result,
"GetCharacters did not return characters in the expected order based on GroupDisplayOrder and DisplayOrder.");
}
// Collect sorts by _languageOrder[m.Id], so every entry in All must appear in
// DisplayOrder. Adding to All without updating DisplayOrder will throw
// KeyNotFoundException at the first GetCharacters call that exercises that language.
// This test verifies the invariant directly so the failure is caught at test time
// rather than at runtime.
[TestMethod]
public void All_EveryEntry_ExistsInDisplayOrder()
{
var displayOrderSet = CharacterMappings.DisplayOrder.ToHashSet();
foreach (var entry in CharacterMappings.All)
{
Assert.IsTrue(
displayOrderSet.Contains(entry.Id),
$"Language.{entry.Id} is in All but missing from DisplayOrder. Add it to DisplayOrder to prevent a KeyNotFoundException at runtime.");
}
}
// GetCharacters for a key that is not mapped in a given language should return empty.
// The test finds a language and an absent key from the live data so it stays valid
// regardless of future mapping changes.
[TestMethod]
public void GetCharacters_UnmappedKey_ReturnsEmpty()
{
var allKeys = Enum.GetValues<LetterKey>().ToHashSet();
var langInfo = CharacterMappings.All.First(l => allKeys.Except(l.Characters.Keys).Any());
var absentKey = allKeys.Except(langInfo.Characters.Keys).First();
var result = CharacterMappings.GetCharacters(absentKey, [langInfo.Id]);
Assert.AreEqual(
0,
result.Length,
$"Expected empty result for Language.{langInfo.Id} / LetterKey.{absentKey}, which has no mapping.");
}
/// <summary>
/// Spoken languages in DisplayOrder should be sorted alphabetically by their enum
/// names to remain culturally neutral.
/// </summary>
[TestMethod]
public void DisplayOrder_SpokenLanguages_AreSortedAlphabeticallyByDisplayName()
{
var spokenLangs = CharacterMappings.DisplayOrder
.Where(lang => CharacterMappings.LanguageLookup[lang].Group == LanguageGroup.Language)
.Select(lang => lang.ToString())
.ToList();
var sorted = spokenLangs.OrderBy(l => l, StringComparer.OrdinalIgnoreCase).ToList();
CollectionAssert.AreEqual(
sorted,
spokenLangs,
"Spoken languages in DisplayOrder should be sorted alphabetically by their enum names.");
}
}

View File

@@ -1,45 +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.VisualStudio.TestTools.UnitTesting;
using PowerAccent.Common;
using WinRtLetterKey = PowerToys.PowerAccentKeyboardService.LetterKey;
namespace PowerAccent.Common.UnitTests;
[TestClass]
public sealed class LetterKeyTests
{
// Verifies that the managed LetterKey enum in PowerAccent.Common stays in sync with the
// WinRT LetterKey enum defined in KeyboardListener.idl. The adapter in PowerAccent.Core
// casts between them via their integer values, so any divergence would silently produce
// wrong character mappings at runtime.
[TestMethod]
public void ManagedLetterKey_MatchesWinRtLetterKey_AllNamesPresent()
{
foreach (WinRtLetterKey winRtValue in Enum.GetValues<WinRtLetterKey>())
{
var name = winRtValue.ToString();
Assert.IsTrue(
Enum.TryParse<LetterKey>(name, out _),
$"WinRT LetterKey.{name} has no corresponding value in the managed LetterKey enum. Update PowerAccent.Common/LetterKey.cs.");
}
}
[TestMethod]
public void ManagedLetterKey_MatchesWinRtLetterKey_ValuesMatch()
{
foreach (WinRtLetterKey winRtValue in Enum.GetValues<WinRtLetterKey>())
{
var name = winRtValue.ToString();
if (Enum.TryParse<LetterKey>(name, out var managedValue))
{
Assert.AreEqual(
(int)(object)winRtValue,
(int)managedValue,
$"LetterKey.{name} has value {(int)(object)winRtValue} in WinRT but {(int)managedValue} in the managed enum. Update PowerAccent.Common/LetterKey.cs.");
}
}
}
}

View File

@@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<AssemblyName>PowerToys.PowerAccent.Common.UnitTests</AssemblyName>
<OutputType>Exe</OutputType>
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\tests\PowerAccent.Common.UnitTests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup>
<CsWinRTIncludes>PowerToys.PowerAccentKeyboardService</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PowerAccent.Common\PowerAccent.Common.csproj" />
<ProjectReference Include="..\PowerAccentKeyboardService\PowerAccentKeyboardService.vcxproj" />
</ItemGroup>
</Project>

View File

@@ -1,791 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Concurrent;
namespace PowerAccent.Common;
/// <summary>
/// Single source of truth for all Quick Accent character data.
/// <para>
/// <see cref="All"/> is the canonical registry of every language: its identity, group,
/// resource identifier, and character mappings. The Settings UI derives its language
/// list from this collection.
/// </para>
/// <para>
/// <see cref="DisplayOrder"/> and <see cref="GroupDisplayOrder"/> control the order
/// in which characters appear in the Quick Accent popup. These are intentionally
/// separate from <see cref="All"/> so that popup ordering is explicit and not an
/// accidental consequence of declaration order.
/// </para>
/// <para>
/// When adding a new language: add a <see cref="Language"/> enum value, a
/// <see cref="LanguageInfo"/> entry to <see cref="All"/>, a position in
/// <see cref="DisplayOrder"/>, and a resx string.
/// </para>
/// </summary>
public static class CharacterMappings
{
/// <summary>
/// The canonical registry of all languages. Each entry defines the language's
/// identity, group, resource identifier, and character mappings.
/// Declaration order here does not affect the popup or settings display order;
/// see <see cref="DisplayOrder"/> and <see cref="GroupDisplayOrder"/> for that.
/// </summary>
public static readonly IReadOnlyList<LanguageInfo> All =
[
new(Language.SPECIAL, "Special", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_0] = ["₀", "⁰", "°", "↉", "₎", "⁾"],
[LetterKey.VK_1] = ["₁", "¹", "½", "⅓", "¼", "⅕", "⅙", "⅐", "⅛", "⅑", "⅒"],
[LetterKey.VK_2] = ["₂", "²", "⅔", "⅖"],
[LetterKey.VK_3] = ["₃", "³", "¾", "⅗", "⅜"],
[LetterKey.VK_4] = ["₄", "⁴", "⅘"],
[LetterKey.VK_5] = ["₅", "⁵", "⅚", "⅝"],
[LetterKey.VK_6] = ["₆", "⁶"],
[LetterKey.VK_7] = ["₇", "⁷", "⅞"],
[LetterKey.VK_8] = ["₈", "⁸", "∞"],
[LetterKey.VK_9] = ["₉", "⁹", "₍", "⁽"],
[LetterKey.VK_A] = ["ȧ", "ǽ", "∀", "ᵃ", "ₐ"],
[LetterKey.VK_B] = ["ḃ", "ᵇ"],
[LetterKey.VK_C] = ["ċ", "°C", "©", "", "∁", "ᶜ"],
[LetterKey.VK_D] = ["ḍ", "ḋ", "∂", "ᵈ"],
[LetterKey.VK_E] = ["∈", "∃", "∄", "∉", "ĕ", "ᵉ", "ₑ"],
[LetterKey.VK_F] = ["ḟ", "°F", "ᶠ"],
[LetterKey.VK_G] = ["ģ", "ǧ", "ġ", "ĝ", "ǥ", "ᵍ"],
[LetterKey.VK_H] = ["ḣ", "ĥ", "ħ", "ʰ", "ₕ"],
[LetterKey.VK_I] = ["ⁱ", "ᵢ"],
[LetterKey.VK_J] = ["ĵ", "ʲ", "ⱼ"],
[LetterKey.VK_K] = ["ķ", "ǩ", "ᵏ", "ₖ"],
[LetterKey.VK_L] = ["ļ", "₺", "ˡ", "ₗ"], // ₺ is in VK_T for other languages, but not VK_L, so we add it here.
[LetterKey.VK_M] = ["ṁ", "ᵐ", "ₘ"],
[LetterKey.VK_N] = ["ņ", "ṅ", "ⁿ", "", "№", "ₙ"],
[LetterKey.VK_O] = ["ȯ", "∅", "⌀", "ᵒ", "ₒ"],
[LetterKey.VK_P] = ["ṗ", "℗", "∏", "¶", "ᵖ", "ₚ"],
[LetterKey.VK_Q] = ["", "𐞥"],
[LetterKey.VK_R] = ["ṙ", "®", "", "ʳ", "ᵣ"],
[LetterKey.VK_S] = ["ṡ", "§", "∑", "∫", "ˢ", "ₛ"],
[LetterKey.VK_T] = ["ţ", "ṫ", "ŧ", "™", "ᵗ", "ₜ"],
[LetterKey.VK_U] = ["ŭ", "ᵘ", "ᵤ"],
[LetterKey.VK_V] = ["V̇", "ᵛ", "ᵥ"],
[LetterKey.VK_W] = ["ẇ", "ʷ"],
[LetterKey.VK_X] = ["ẋ", "×", "ˣ", "ₓ"],
[LetterKey.VK_Y] = ["ẏ", "ꝡ", "ʸ"],
[LetterKey.VK_Z] = ["ʒ", "ǯ", "", "ᶻ"],
[LetterKey.VK_COMMA] = ["∙", "₋", "⁻", "", "√", "‟", "《", "》", "", "〈", "〉", "″", "‴", "⁗"], // is in VK_MINUS for other languages, but not VK_COMMA, so we add it here.
[LetterKey.VK_PERIOD] = ["…", "⁝", "\u0300", "\u0301", "\u0302", "\u0303", "\u0304", "\u0308", "\u030B", "\u030C"],
[LetterKey.VK_MINUS] = ["~", "", "", "", "", "—", "―", "", "", "⸺", "⸻", "∓", "₋", "⁻"],
[LetterKey.VK_SLASH_] = ["÷", "√"],
[LetterKey.VK_DIVIDE_] = ["÷", "√"],
[LetterKey.VK_MULTIPLY_] = ["×", "⋅", "ˣ", "ₓ"],
[LetterKey.VK_PLUS] = ["≤", "≥", "≠", "≈", "≙", "⊕", "⊗", "±", "≅", "≡", "₊", "⁺", "₌", "⁼"],
[LetterKey.VK_BACKSLASH] = ["`", "~"],
}),
new(Language.BG, "Bulgarian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_I] = ["й"],
}),
new(Language.CA, "Catalan", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["à", "á"],
[LetterKey.VK_C] = ["ç"],
[LetterKey.VK_E] = ["è", "é", "€"],
[LetterKey.VK_I] = ["ì", "í", "ï"],
[LetterKey.VK_N] = ["ñ"],
[LetterKey.VK_O] = ["ò", "ó"],
[LetterKey.VK_U] = ["ù", "ú", "ü"],
[LetterKey.VK_L] = ["·"],
[LetterKey.VK_COMMA] = ["¿", "?", "¡", "!", "«", "»", "“", "”", "", ""],
}),
new(Language.CRH, "Crimean", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["â"],
[LetterKey.VK_C] = ["ç"],
[LetterKey.VK_E] = ["€"],
[LetterKey.VK_G] = ["ğ"],
[LetterKey.VK_H] = ["₴"],
[LetterKey.VK_I] = ["ı", "İ"],
[LetterKey.VK_N] = ["ñ"],
[LetterKey.VK_O] = ["ö"],
[LetterKey.VK_S] = ["ş"],
[LetterKey.VK_T] = ["₺"],
[LetterKey.VK_U] = ["ü"],
}),
// Currency symbols. This is a "special" language group as it's not a spoken
// language, but rather a set of symbols used across languages.
new(Language.CUR, "Currency", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_B] = ["฿", "в"],
[LetterKey.VK_C] = ["¢", "₡", "č"],
[LetterKey.VK_D] = ["₫"],
[LetterKey.VK_E] = ["€"],
[LetterKey.VK_F] = ["ƒ"],
[LetterKey.VK_H] = ["₴"],
[LetterKey.VK_K] = ["₭"],
[LetterKey.VK_L] = ["ł"],
[LetterKey.VK_N] = ["л"],
[LetterKey.VK_M] = ["₼"],
[LetterKey.VK_P] = ["£", "₽"],
[LetterKey.VK_R] = ["₹", "៛", "﷼"],
[LetterKey.VK_S] = ["$", "₪"],
[LetterKey.VK_T] = ["₮", "₺", "₸"],
[LetterKey.VK_W] = ["₩"],
[LetterKey.VK_Y] = ["¥"],
[LetterKey.VK_Z] = ["z"],
}),
new(Language.CY, "Welsh", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["â", "ä", "à", "á"],
[LetterKey.VK_E] = ["ê", "ë", "è", "é"],
[LetterKey.VK_I] = ["î", "ï", "ì", "í"],
[LetterKey.VK_O] = ["ô", "ö", "ò", "ó"],
[LetterKey.VK_P] = ["£"],
[LetterKey.VK_U] = ["û", "ü", "ù", "ú"],
[LetterKey.VK_Y] = ["ŷ", "ÿ", "ỳ", "ý"],
[LetterKey.VK_W] = ["ŵ", "ẅ", "ẁ", "ẃ"],
[LetterKey.VK_COMMA] = ["", "", "“", "”"],
}),
new(Language.CZ, "Czech", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["á"],
[LetterKey.VK_C] = ["č"],
[LetterKey.VK_D] = ["ď"],
[LetterKey.VK_E] = ["ě", "é"],
[LetterKey.VK_I] = ["í"],
[LetterKey.VK_N] = ["ň"],
[LetterKey.VK_O] = ["ó"],
[LetterKey.VK_R] = ["ř"],
[LetterKey.VK_S] = ["š"],
[LetterKey.VK_T] = ["ť"],
[LetterKey.VK_U] = ["ů", "ú"],
[LetterKey.VK_Y] = ["ý"],
[LetterKey.VK_Z] = ["ž"],
[LetterKey.VK_COMMA] = ["„", "“", "", "", "»", "«", "", ""],
}),
new(Language.DK, "Danish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["å", "æ"],
[LetterKey.VK_E] = ["€"],
[LetterKey.VK_O] = ["ø"],
[LetterKey.VK_COMMA] = ["»", "«", "“", "”", "", "", "", ""],
}),
// Gaelic (Irish).
new(Language.GA, "Gaeilge", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["á"],
[LetterKey.VK_E] = ["é", "€"],
[LetterKey.VK_I] = ["í"],
[LetterKey.VK_O] = ["ó"],
[LetterKey.VK_U] = ["ú"],
[LetterKey.VK_COMMA] = ["“", "”", "", ""],
}),
// Gaelic (Scottish).
new(Language.GD, "Gaidhlig", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["à"],
[LetterKey.VK_E] = ["è"],
[LetterKey.VK_I] = ["ì"],
[LetterKey.VK_O] = ["ò"],
[LetterKey.VK_P] = ["£"],
[LetterKey.VK_U] = ["ù"],
[LetterKey.VK_COMMA] = ["“", "”", "", ""],
}),
new(Language.DE, "German", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["ä"],
[LetterKey.VK_E] = ["€"],
[LetterKey.VK_O] = ["ö"],
[LetterKey.VK_S] = ["ß"],
[LetterKey.VK_U] = ["ü"],
[LetterKey.VK_COMMA] = ["„", "“", "", "", "»", "«", "", ""],
}),
new(Language.EL, "Greek", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["α", "ά"],
[LetterKey.VK_B] = ["β"],
[LetterKey.VK_C] = ["χ"],
[LetterKey.VK_D] = ["δ"],
[LetterKey.VK_E] = ["ε", "έ", "η", "ή"],
[LetterKey.VK_F] = ["φ"],
[LetterKey.VK_G] = ["γ"],
[LetterKey.VK_I] = ["ι", "ί"],
[LetterKey.VK_K] = ["κ"],
[LetterKey.VK_L] = ["λ"],
[LetterKey.VK_M] = ["μ"],
[LetterKey.VK_N] = ["ν"],
[LetterKey.VK_O] = ["ο", "ό", "ω", "ώ"],
[LetterKey.VK_P] = ["π", "φ", "ψ"],
[LetterKey.VK_R] = ["ρ"],
[LetterKey.VK_S] = ["σ", "ς"],
[LetterKey.VK_T] = ["τ", "θ", "ϑ"],
[LetterKey.VK_U] = ["υ", "ύ"],
[LetterKey.VK_X] = ["ξ"],
[LetterKey.VK_Y] = ["υ"],
[LetterKey.VK_Z] = ["ζ"],
[LetterKey.VK_COMMA] = ["“", "”", "«", "»"],
}),
new(Language.EST, "Estonian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["ä"],
[LetterKey.VK_E] = ["€"],
[LetterKey.VK_O] = ["ö", "õ"],
[LetterKey.VK_U] = ["ü"],
[LetterKey.VK_Z] = ["ž"],
[LetterKey.VK_S] = ["š"],
[LetterKey.VK_COMMA] = ["„", "“", "«", "»"],
}),
new(Language.EPO, "Esperanto", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_C] = ["ĉ"],
[LetterKey.VK_G] = ["ĝ"],
[LetterKey.VK_H] = ["ĥ"],
[LetterKey.VK_J] = ["ĵ"],
[LetterKey.VK_S] = ["ŝ"],
[LetterKey.VK_U] = ["ŭ"],
}),
new(Language.FI, "Finnish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["ä", "å"],
[LetterKey.VK_E] = ["€"],
[LetterKey.VK_O] = ["ö"],
[LetterKey.VK_COMMA] = ["”", "", "»"],
}),
new(Language.FR, "French", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["à", "â", "á", "ä", "ã", "æ"],
[LetterKey.VK_C] = ["ç"],
[LetterKey.VK_E] = ["é", "è", "ê", "ë", "€"],
[LetterKey.VK_I] = ["î", "ï", "í", "ì"],
[LetterKey.VK_O] = ["ô", "ö", "ó", "ò", "õ", "œ"],
[LetterKey.VK_U] = ["û", "ù", "ü", "ú"],
[LetterKey.VK_Y] = ["ÿ", "ý"],
[LetterKey.VK_COMMA] = ["«", "»", "", "", "“", "”", "", ""],
}),
new(Language.GRC, "Greek_Polytonic", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["α", "ἀ", "ἁ", "ὰ", "ά", "ᾶ", "ᾱ", "ᾰ", "ἂ", "ἃ", "ἄ", "ἅ", "ἆ", "ἇ", "ᾳ", "ᾀ", "ᾁ", "ᾴ", "ᾲ", "ᾷ", "ᾄ", "ᾅ", "ᾂ", "ᾃ", "ᾆ", "ᾇ"],
[LetterKey.VK_B] = ["β"],
[LetterKey.VK_C] = ["χ", "ϲ"],
[LetterKey.VK_D] = ["δ"],
[LetterKey.VK_E] = ["ε", "ἐ", "ἑ", "ὲ", "έ", "ἒ", "ἓ", "ἔ", "ἕ"],
[LetterKey.VK_F] = ["φ", "ϝ"],
[LetterKey.VK_G] = ["γ"],
[LetterKey.VK_H] = ["η", "ἠ", "ἡ", "ὴ", "ή", "ῆ", "ἢ", "ἣ", "ἤ", "ἥ", "ἦ", "ἧ", "ῃ", "ᾐ", "ᾑ", "ῄ", "ῂ", "ῇ", "ᾔ", "ᾕ", "ᾒ", "ᾓ", "ᾖ", "ᾗ"],
[LetterKey.VK_I] = ["ι", "ἰ", "ἱ", "ὶ", "ί", "ῖ", "ῑ", "ῐ", "ἲ", "ἳ", "ἴ", "ἵ", "ἶ", "ἷ", "ϊ", "ΐ", "ῒ", "ῗ"],
[LetterKey.VK_K] = ["κ"],
[LetterKey.VK_L] = ["λ"],
[LetterKey.VK_M] = ["μ"],
[LetterKey.VK_N] = ["ν"],
[LetterKey.VK_O] = ["ο", "ὀ", "ὁ", "ὸ", "ό", "ὂ", "ὃ", "ὄ", "ὅ"],
[LetterKey.VK_P] = ["π", "φ", "ψ", "ρ"],
[LetterKey.VK_Q] = ["ϙ", "ϟ"],
[LetterKey.VK_R] = ["ρ", "ῤ", "ῥ"],
[LetterKey.VK_S] = ["σ", "ς", "ϛ", "ϲ", "ϡ"],
[LetterKey.VK_T] = ["τ", "θ", "ϑ"],
[LetterKey.VK_U] = ["υ", "ὐ", "ὑ", "ὺ", "ύ", "ῦ", "ῡ", "ῠ", "ὒ", "ὓ", "ὔ", "ὕ", "ὖ", "ὗ", "ϋ", "ΰ", "ῢ", "ῧ"],
[LetterKey.VK_V] = ["β", "ϝ"],
[LetterKey.VK_W] = ["ω", "ὠ", "ὡ", "ὼ", "ώ", "ῶ", "ὢ", "ὣ", "ὤ", "ὥ", "ὦ", "ὧ", "ῳ", "ᾠ", "ᾡ", "ῴ", "ῲ", "ῷ", "ᾤ", "ᾥ", "ᾢ", "ᾣ", "ᾦ", "ᾧ"],
[LetterKey.VK_X] = ["ξ", "χ"],
[LetterKey.VK_Y] = ["υ", "ὐ", "ὑ", "ὺ", "ύ", "ῦ", "ῡ", "ῠ", "ὒ", "ὓ", "ὔ", "ὕ", "ὖ", "ὗ", "ϋ", "ΰ", "ῢ", "ῧ"],
[LetterKey.VK_Z] = ["ζ"],
[LetterKey.VK_COMMA] = ["“", "”", "", "", ";", "`", "´"],
[LetterKey.VK_PERIOD] = ["·"],
}),
new(Language.HR, "Croatian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_C] = ["ć", "č"],
[LetterKey.VK_D] = ["đ"],
[LetterKey.VK_E] = ["€"],
[LetterKey.VK_S] = ["š"],
[LetterKey.VK_Z] = ["ž"],
[LetterKey.VK_COMMA] = ["„", "“", "»", "«"],
}),
new(Language.HE, "Hebrew", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["שׂ", "שׁ", "\u05b0"],
[LetterKey.VK_B] = ["׆"],
[LetterKey.VK_E] = ["\u05b8", "\u05b3", "\u05bb"],
[LetterKey.VK_G] = ["ױ"],
[LetterKey.VK_H] = ["ײ", "ײַ", "ׯ", "\u05b4"],
[LetterKey.VK_M] = ["\u05b5"],
[LetterKey.VK_P] = ["\u05b7", "\u05b2"],
[LetterKey.VK_S] = ["\u05bc"],
[LetterKey.VK_T] = ["ﭏ"],
[LetterKey.VK_U] = ["וֹ", "וּ", "װ", "\u05b9"],
[LetterKey.VK_X] = ["\u05b6", "\u05b1"],
[LetterKey.VK_Y] = ["ױ"],
[LetterKey.VK_COMMA] = ["”", "", "'", "״", "׳"],
[LetterKey.VK_PERIOD] = ["\u05ab", "\u05bd", "\u05bf"],
[LetterKey.VK_MINUS] = ["־"],
}),
new(Language.HU, "Hungarian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["á"],
[LetterKey.VK_E] = ["é"],
[LetterKey.VK_I] = ["í"],
[LetterKey.VK_O] = ["ó", "ő", "ö"],
[LetterKey.VK_U] = ["ú", "ű", "ü"],
[LetterKey.VK_Y] = ["ÿ", "ý"],
[LetterKey.VK_COMMA] = ["„", "”", "»", "«"],
}),
new(Language.IS, "Icelandic", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["á", "æ"],
[LetterKey.VK_D] = ["ð"],
[LetterKey.VK_E] = ["é"],
[LetterKey.VK_I] = ["í"],
[LetterKey.VK_O] = ["ó", "ö"],
[LetterKey.VK_U] = ["ú"],
[LetterKey.VK_Y] = ["ý"],
[LetterKey.VK_T] = ["þ"],
[LetterKey.VK_COMMA] = ["„", "“", "", ""],
}),
// International Phonetic Alphabet. This is a "special" language group as it's not
// a spoken language, but rather a set of symbols used across languages.
new(Language.IPA, "IPA", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["ɐ", "ɑ", "ɒ", "ǎ"],
[LetterKey.VK_B] = ["ʙ"],
[LetterKey.VK_E] = ["ɘ", "ɵ", "ə", "ɛ", "ɜ", "ɞ"],
[LetterKey.VK_F] = ["ɟ", "ɸ"],
[LetterKey.VK_G] = ["ɢ", "ɣ"],
[LetterKey.VK_H] = ["ɦ", "ʜ"],
[LetterKey.VK_I] = ["ɨ", "ɪ"],
[LetterKey.VK_J] = ["ʝ"],
[LetterKey.VK_L] = ["ɬ", "ɮ", "ꞎ", "ɭ", "ʎ", "ʟ", "ɺ"],
[LetterKey.VK_N] = ["ɳ", "ɲ", "ŋ", "ɴ"],
[LetterKey.VK_O] = ["ɤ", "ɔ", "ɶ", "ǒ"],
[LetterKey.VK_R] = ["ʁ", "ɹ", "ɻ", "ɾ", "ɽ", "ʀ"],
[LetterKey.VK_S] = ["ʃ", "ʂ", "ɕ"],
[LetterKey.VK_U] = ["ʉ", "ʊ", "ǔ"],
[LetterKey.VK_V] = ["ʋ", "ⱱ", "ʌ"],
[LetterKey.VK_W] = ["ɰ", "ɯ"],
[LetterKey.VK_Y] = ["ʏ"],
[LetterKey.VK_Z] = ["ʒ", "ʐ", "ʑ"],
[LetterKey.VK_COMMA] = ["ʡ", "ʔ", "ʕ", "ʢ"],
}),
new(Language.IT, "Italian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["à"],
[LetterKey.VK_E] = ["è", "é", "ə", "€"],
[LetterKey.VK_I] = ["ì", "í"],
[LetterKey.VK_O] = ["ò", "ó"],
[LetterKey.VK_U] = ["ù", "ú"],
[LetterKey.VK_COMMA] = ["«", "»", "“", "”", "", ""],
}),
new(Language.KU, "Kurdish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_C] = ["ç"],
[LetterKey.VK_E] = ["ê", "€"],
[LetterKey.VK_I] = ["î"],
[LetterKey.VK_O] = ["ö", "ô"],
[LetterKey.VK_L] = ["ł"],
[LetterKey.VK_N] = ["ň"],
[LetterKey.VK_R] = ["ř"],
[LetterKey.VK_S] = ["ş"],
[LetterKey.VK_U] = ["û", "ü"],
[LetterKey.VK_COMMA] = ["«", "»", "“", "”"],
}),
new(Language.LT, "Lithuanian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["ą"],
[LetterKey.VK_C] = ["č"],
[LetterKey.VK_E] = ["ę", "ė", "€"],
[LetterKey.VK_I] = ["į"],
[LetterKey.VK_S] = ["š"],
[LetterKey.VK_U] = ["ų", "ū"],
[LetterKey.VK_Z] = ["ž"],
[LetterKey.VK_COMMA] = ["„", "“", "", ""],
}),
new(Language.MK, "Macedonian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_E] = ["ѐ"],
[LetterKey.VK_I] = ["ѝ"],
[LetterKey.VK_COMMA] = ["„", "“", "", ""],
}),
new(Language.MT, "Maltese", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["à"],
[LetterKey.VK_C] = ["ċ"],
[LetterKey.VK_E] = ["è", "€"],
[LetterKey.VK_G] = ["ġ"],
[LetterKey.VK_H] = ["ħ"],
[LetterKey.VK_I] = ["ì"],
[LetterKey.VK_O] = ["ò"],
[LetterKey.VK_U] = ["ù"],
[LetterKey.VK_Z] = ["ż"],
}),
new(Language.MI, "Maori", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["ā"],
[LetterKey.VK_E] = ["ē"],
[LetterKey.VK_I] = ["ī"],
[LetterKey.VK_O] = ["ō"],
[LetterKey.VK_S] = ["$"],
[LetterKey.VK_U] = ["ū"],
[LetterKey.VK_COMMA] = ["“", "”", "", ""],
}),
new(Language.NL, "Dutch", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["á", "à", "ä"],
[LetterKey.VK_C] = ["ç"],
[LetterKey.VK_E] = ["é", "è", "ë", "ê", "€"],
[LetterKey.VK_I] = ["í", "ï", "î"],
[LetterKey.VK_N] = ["ñ"],
[LetterKey.VK_O] = ["ó", "ö", "ô"],
[LetterKey.VK_U] = ["ú", "ü", "û"],
[LetterKey.VK_COMMA] = ["“", "„", "”", "", ",", ""],
}),
new(Language.NO, "Norwegian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["å", "æ"],
[LetterKey.VK_E] = ["€", "é"],
[LetterKey.VK_O] = ["ø"],
[LetterKey.VK_S] = ["$"],
[LetterKey.VK_COMMA] = ["«", "»", ",", "", "", "„", "“"],
}),
new(Language.PI, "Pinyin", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_1] = ["\u0304", "ˉ"],
[LetterKey.VK_2] = ["\u0301", "ˊ"],
[LetterKey.VK_3] = ["\u030c", "ˇ"],
[LetterKey.VK_4] = ["\u0300", "ˋ"],
[LetterKey.VK_5] = ["·"],
[LetterKey.VK_A] = ["ā", "á", "ǎ", "à", "ɑ", "ɑ\u0304", "ɑ\u0301", "ɑ\u030c", "ɑ\u0300"],
[LetterKey.VK_C] = ["ĉ"],
[LetterKey.VK_E] = ["ē", "é", "ě", "è", "ê", "ê\u0304", "ế", "ê\u030c", "ề"],
[LetterKey.VK_I] = ["ī", "í", "ǐ", "ì"],
[LetterKey.VK_M] = ["m\u0304", "ḿ", "m\u030c", "m\u0300"],
[LetterKey.VK_N] = ["n\u0304", "ń", "ň", "ǹ", "ŋ", "ŋ\u0304", "ŋ\u0301", "ŋ\u030c", "ŋ\u0300"],
[LetterKey.VK_O] = ["ō", "ó", "ǒ", "ò"],
[LetterKey.VK_S] = ["ŝ"],
[LetterKey.VK_U] = ["ū", "ú", "ǔ", "ù", "ü", "ǖ", "ǘ", "ǚ", "ǜ"],
[LetterKey.VK_V] = ["ü", "ǖ", "ǘ", "ǚ", "ǜ"],
[LetterKey.VK_Y] = ["¥"],
[LetterKey.VK_Z] = ["ẑ"],
[LetterKey.VK_COMMA] = ["“", "”", "", "", "「", "」", "『", "』"],
}),
// Proto-Indo-European. This is a "special" language group as it's not a spoken
// language, but rather a reconstructed ancestor of many languages.
new(Language.PIE, "Proto_Indo_European", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["ā"],
[LetterKey.VK_E] = ["ē"],
[LetterKey.VK_O] = ["ō"],
[LetterKey.VK_K] = ["ḱ"],
[LetterKey.VK_G] = ["ǵ"],
[LetterKey.VK_R] = ["r̥"],
[LetterKey.VK_L] = ["l̥"],
[LetterKey.VK_M] = ["m̥"],
[LetterKey.VK_N] = ["n̥"],
}),
new(Language.PL, "Polish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["ą"],
[LetterKey.VK_C] = ["ć"],
[LetterKey.VK_E] = ["ę", "€"],
[LetterKey.VK_L] = ["ł"],
[LetterKey.VK_N] = ["ń"],
[LetterKey.VK_O] = ["ó"],
[LetterKey.VK_S] = ["ś"],
[LetterKey.VK_Z] = ["ż", "ź"],
[LetterKey.VK_COMMA] = ["„", "”", "", "", "»", "«"],
}),
new(Language.PT, "Portuguese", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["á", "à", "â", "ã", "ª"],
[LetterKey.VK_C] = ["ç"],
[LetterKey.VK_E] = ["é", "ê", "€"],
[LetterKey.VK_I] = ["í"],
[LetterKey.VK_O] = ["ô", "ó", "õ", "º"],
[LetterKey.VK_S] = ["$"],
[LetterKey.VK_U] = ["ú"],
[LetterKey.VK_COMMA] = ["“", "”", "", "", "«", "»"],
}),
new(Language.RO, "Romanian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["ă", "â"],
[LetterKey.VK_I] = ["î"],
[LetterKey.VK_S] = ["ș"],
[LetterKey.VK_T] = ["ț"],
[LetterKey.VK_COMMA] = ["„", "”", "«", "»"],
}),
// Middle Eastern Romanization. This is a "special" language group as it's not a
// spoken language, but rather a set of characters used to romanize various Middle
// Eastern languages.
new(Language.ROM, "Romanization", LanguageGroup.Special, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["á", "â", "ă", "ā"],
[LetterKey.VK_B] = ["ḇ"],
[LetterKey.VK_C] = ["č", "ç"],
[LetterKey.VK_D] = ["ḑ", "ḍ", "ḏ", "ḏ\u0323"],
[LetterKey.VK_E] = ["ê", "ě", "ĕ", "ē", "é", "ə"],
[LetterKey.VK_G] = ["ġ", "ǧ", "ğ", "ḡ", "g\u0303", "g\u0331"],
[LetterKey.VK_H] = ["ḧ", "ḩ", "ḥ", "ḫ", "h\u0331"],
[LetterKey.VK_I] = ["í", "ı", "î", "ī", "ı\u0307\u0304"],
[LetterKey.VK_J] = ["ǰ", "j\u0331"],
[LetterKey.VK_K] = ["ḳ", "ḵ"],
[LetterKey.VK_L] = ["ł"],
[LetterKey.VK_N] = ["ⁿ", "ñ"],
[LetterKey.VK_O] = ["ó", "ô", "ö", "ŏ", "ō", "ȫ"],
[LetterKey.VK_P] = ["p\u0304"],
[LetterKey.VK_R] = ["ṙ", "ṛ"],
[LetterKey.VK_S] = ["ś", "š", "ş", "ṣ", "s\u0331", "ṣ\u0304"],
[LetterKey.VK_T] = ["ẗ", "ţ", "ṭ", "ṯ"],
[LetterKey.VK_U] = ["ú", "û", "ü", "ū", "ǖ"],
[LetterKey.VK_V] = ["v\u0307", "ṿ", "ᵛ"],
[LetterKey.VK_Y] = ["̀y"],
[LetterKey.VK_Z] = ["ż", "ž", "z\u0304", "z\u0327", "ẓ", "z\u0324", "ẕ"],
[LetterKey.VK_PERIOD] = ["", "ʾ", "ʿ", "", "…"],
}),
new(Language.SK, "Slovak", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["á", "ä"],
[LetterKey.VK_C] = ["č"],
[LetterKey.VK_D] = ["ď"],
[LetterKey.VK_E] = ["é", "€"],
[LetterKey.VK_I] = ["í"],
[LetterKey.VK_L] = ["ľ", "ĺ"],
[LetterKey.VK_N] = ["ň"],
[LetterKey.VK_O] = ["ó", "ô"],
[LetterKey.VK_R] = ["ŕ"],
[LetterKey.VK_S] = ["š"],
[LetterKey.VK_T] = ["ť"],
[LetterKey.VK_U] = ["ú"],
[LetterKey.VK_Y] = ["ý"],
[LetterKey.VK_Z] = ["ž"],
[LetterKey.VK_COMMA] = ["„", "“", "", "", "»", "«", "", ""],
}),
new(Language.SL, "Slovenian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_C] = ["č", "ć"],
[LetterKey.VK_E] = ["€"],
[LetterKey.VK_S] = ["š"],
[LetterKey.VK_Z] = ["ž"],
[LetterKey.VK_COMMA] = ["„", "“", "»", "«"],
}),
new(Language.SP, "Spanish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["á"],
[LetterKey.VK_E] = ["é", "€"],
[LetterKey.VK_H] = ["ḥ"],
[LetterKey.VK_I] = ["í"],
[LetterKey.VK_L] = ["ḷ"],
[LetterKey.VK_N] = ["ñ"],
[LetterKey.VK_O] = ["ó"],
[LetterKey.VK_U] = ["ú", "ü"],
[LetterKey.VK_COMMA] = ["¿", "?", "¡", "!", "«", "»", "“", "”", "", ""],
}),
new(Language.SR, "Serbian", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_C] = ["ć", "č"],
[LetterKey.VK_D] = ["đ"],
[LetterKey.VK_S] = ["š"],
[LetterKey.VK_Z] = ["ž"],
[LetterKey.VK_COMMA] = ["„", "“", "", "", "»", "«", "", ""],
}),
new(Language.SR_CYRL, "Serbian_Cyrillic", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_D] = ["ђ", "џ"],
[LetterKey.VK_L] = ["љ"],
[LetterKey.VK_N] = ["њ"],
[LetterKey.VK_C] = ["ћ"],
}),
new(Language.SV, "Swedish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["å", "ä"],
[LetterKey.VK_E] = ["é"],
[LetterKey.VK_O] = ["ö"],
[LetterKey.VK_COMMA] = ["”", "", "»", "«"],
}),
new(Language.TK, "Turkish", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["â"],
[LetterKey.VK_C] = ["ç"],
[LetterKey.VK_E] = ["ë", "€"],
[LetterKey.VK_G] = ["ğ"],
[LetterKey.VK_I] = ["ı", "İ", "î",],
[LetterKey.VK_O] = ["ö", "ô"],
[LetterKey.VK_S] = ["ş"],
[LetterKey.VK_T] = ["₺"],
[LetterKey.VK_U] = ["ü", "û"],
[LetterKey.VK_COMMA] = ["“", "”", "", "", "«", "»", "", ""],
}),
new(Language.VI, "Vietnamese", LanguageGroup.Language, new Dictionary<LetterKey, string[]>
{
[LetterKey.VK_A] = ["à", "ả", "ã", "á", "ạ", "ă", "ằ", "ẳ", "ẵ", "ắ", "ặ", "â", "ầ", "ẩ", "ẫ", "ấ", "ậ"],
[LetterKey.VK_D] = ["đ"],
[LetterKey.VK_E] = ["è", "ẻ", "ẽ", "é", "ẹ", "ê", "ề", "ể", "ễ", "ế", "ệ"],
[LetterKey.VK_I] = ["ì", "ỉ", "ĩ", "í", "ị"],
[LetterKey.VK_O] = ["ò", "ỏ", "õ", "ó", "ọ", "ô", "ồ", "ổ", "ỗ", "ố", "ộ", "ơ", "ờ", "ở", "ỡ", "ớ", "ợ"],
[LetterKey.VK_U] = ["ù", "ủ", "ũ", "ú", "ụ", "ư", "ừ", "ử", "ữ", "ứ", "ự"],
[LetterKey.VK_Y] = ["ỳ", "ỷ", "ỹ", "ý", "ỵ"],
}),
];
/// <summary>
/// O(1) lookup from <see cref="Language"/> to its <see cref="LanguageInfo"/>.
/// Use this instead of searching <see cref="All"/> when you have a language identity.
/// </summary>
public static readonly IReadOnlyDictionary<Language, LanguageInfo> LanguageLookup =
All.ToDictionary(x => x.Id);
/// <summary>
/// The order in which language groups appear in the Quick Accent popup.
/// Groups listed first have their characters shown first.
/// This is intentionally separate from the <see cref="LanguageGroup"/> enum order.
/// </summary>
public static readonly IReadOnlyList<LanguageGroup> GroupDisplayOrder =
[
LanguageGroup.UserDefined,
LanguageGroup.Language,
LanguageGroup.Special,
];
/// <summary>
/// The order in which individual languages appear within their group in the Quick
/// Accent popup. Position in this list is the display order; position in
/// <see cref="All"/> is irrelevant for popup ordering.
/// Entries are sorted alphabetically by <see cref="Language"/> enum name.
/// When adding a new language, insert it in alphabetical order.
/// </summary>
public static readonly IReadOnlyList<Language> DisplayOrder =
[
// Spoken languages.
Language.BG,
Language.CA,
Language.CRH,
Language.CY,
Language.CZ,
Language.DE,
Language.DK,
Language.EL,
Language.EPO,
Language.EST,
Language.FI,
Language.FR,
Language.GA,
Language.GD,
Language.HE,
Language.HR,
Language.HU,
Language.IS,
Language.IT,
Language.KU,
Language.LT,
Language.MI,
Language.MK,
Language.MT,
Language.NL,
Language.NO,
Language.PI,
Language.PL,
Language.PT,
Language.RO,
Language.SK,
Language.SL,
Language.SP,
Language.SR,
Language.SR_CYRL,
Language.SV,
Language.TK,
Language.VI,
// Symbols, non-spoken languages, and non-language-specific characters.
Language.CUR,
Language.GRC,
Language.IPA,
Language.PIE,
Language.ROM,
Language.SPECIAL,
];
// O(1) sort-key lookups derived from the display order lists above.
private static readonly Dictionary<LanguageGroup, int> _groupOrder =
GroupDisplayOrder.Select((g, i) => (g, i)).ToDictionary(x => x.g, x => x.i);
private static readonly Dictionary<Language, int> _languageOrder =
DisplayOrder.Select((l, i) => (l, i)).ToDictionary(x => x.l, x => x.i);
private static readonly ConcurrentDictionary<LetterKey, string[]> _allLanguagesCache = new();
/// <summary>
/// Returns the deduplicated set of characters for the given key across the specified
/// languages, ordered by <see cref="GroupDisplayOrder"/> then <see cref="DisplayOrder"/>.
/// </summary>
public static string[] GetCharacters(LetterKey letter, Language[] langs)
{
if (langs.Length == 0)
{
return [];
}
if (langs.Length == All.Count)
{
return _allLanguagesCache.GetOrAdd(letter, key => Collect(key, All));
}
return Collect(letter, langs.Select(lang => LanguageLookup[lang]));
}
private static string[] Collect(LetterKey letter, IEnumerable<LanguageInfo> maps)
{
var result = new List<string>();
foreach (var map in maps
.OrderBy(m => _groupOrder[m.Group])
.ThenBy(m => _languageOrder[m.Id]))
{
if (map.Characters.TryGetValue(letter, out var chars))
{
result.AddRange(chars);
}
}
return [.. result.Distinct()];
}
}

View File

@@ -1,53 +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.
namespace PowerAccent.Common;
public enum Language
{
SPECIAL,
BG,
CA,
CRH,
CUR,
CY,
CZ,
DK,
GA,
GD,
DE,
EL,
EST,
EPO,
FI,
FR,
GRC,
HR,
HE,
HU,
IS,
IPA,
IT,
KU,
LT,
MK,
MT,
MI,
NL,
NO,
PI,
PIE,
PL,
PT,
RO,
ROM,
SK,
SL,
SP,
SR,
SR_CYRL,
SV,
TK,
VI,
}

View File

@@ -1,20 +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.
namespace PowerAccent.Common;
/// <summary>
/// Describes which category a language belongs to in the Quick Accent settings UI.
/// </summary>
public enum LanguageGroup
{
/// <summary>Standard spoken languages.</summary>
Language,
/// <summary>Special character sets (e.g. currencies, IPA, romanization).</summary>
Special,
/// <summary>User-defined custom character sets.</summary>
UserDefined,
}

View File

@@ -1,24 +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.
namespace PowerAccent.Common;
/// <summary>
/// Describes a single language entry: its enum identity, the resource key identifier
/// used to look up its localized display name, which group it belongs to, and its
/// character mappings.
/// </summary>
/// <param name="Id">The <see cref="Language"/> enum value for this entry.</param>
/// <param name="Identifier">
/// The stable string identifier used to construct the settings resource key
/// (e.g. <c>"Bulgarian"</c> -> <c>QuickAccent_SelectedLanguage_Bulgarian</c>).
/// </param>
/// <param name="Group">Which <see cref="LanguageGroup"/> category this entry belongs to.
/// </param>
/// <param name="Characters">The character mappings for this language.</param>
public sealed record LanguageInfo(
Language Id,
string Identifier,
LanguageGroup Group,
IReadOnlyDictionary<LetterKey, string[]> Characters);

View File

@@ -1,58 +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.
namespace PowerAccent.Common;
// Mirrors the LetterKey enum defined in PowerAccentKeyboardService\KeyboardListener.idl.
// The numeric values must stay in sync with the IDL definition.
// This managed copy exists so that language mapping data in CharacterMappings.cs can be shared
// with projects (e.g. Settings UI) that do not reference the WinRT keyboard service.
public enum LetterKey
{
None = 0x00,
VK_0 = 0x30,
VK_1 = 0x31,
VK_2 = 0x32,
VK_3 = 0x33,
VK_4 = 0x34,
VK_5 = 0x35,
VK_6 = 0x36,
VK_7 = 0x37,
VK_8 = 0x38,
VK_9 = 0x39,
VK_A = 0x41,
VK_B = 0x42,
VK_C = 0x43,
VK_D = 0x44,
VK_E = 0x45,
VK_F = 0x46,
VK_G = 0x47,
VK_H = 0x48,
VK_I = 0x49,
VK_J = 0x4A,
VK_K = 0x4B,
VK_L = 0x4C,
VK_M = 0x4D,
VK_N = 0x4E,
VK_O = 0x4F,
VK_P = 0x50,
VK_Q = 0x51,
VK_R = 0x52,
VK_S = 0x53,
VK_T = 0x54,
VK_U = 0x55,
VK_V = 0x56,
VK_W = 0x57,
VK_X = 0x58,
VK_Y = 0x59,
VK_Z = 0x5A,
VK_PLUS = 0xBB,
VK_COMMA = 0xBC,
VK_PERIOD = 0xBE,
VK_MINUS = 0xBD,
VK_MULTIPLY_ = 0x6A,
VK_SLASH_ = 0xBF,
VK_DIVIDE_ = 0x6F,
VK_BACKSLASH = 0xDC,
}

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<PropertyGroup>
<!-- Currently hard-coded, as this project does not target WinRT.
To be removed after non-WinRT information is moved from
Common.Dotnet.CsWinRT.props. -->
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -1,26 +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.
// Character mapping data has moved to PowerAccent.Common.CharacterMappings.
// This file provides a thin adapter so that callers in PowerAccent.Core can pass the
// WinRT LetterKey (from PowerAccentKeyboardService) without needing to know about the
// managed copy defined in PowerAccent.Common.
using CommonLanguage = global::PowerAccent.Common.Language;
using CommonLetterKey = global::PowerAccent.Common.LetterKey;
using CommonMappings = global::PowerAccent.Common.CharacterMappings;
using WinRtLetterKey = PowerToys.PowerAccentKeyboardService.LetterKey;
namespace PowerAccent.Core;
internal static class CharacterMappings
{
public static string[] GetCharacters(WinRtLetterKey letter, CommonLanguage[] langs)
{
// The managed and WinRT LetterKey enums share identical numeric values, so a
// direct cast via int is safe. If the IDL values ever change, the unit tests
// in PowerAccent.Common will catch the mismatch.
var managedKey = (CommonLetterKey)(int)letter;
return CommonMappings.GetCharacters(managedKey, langs);
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -89,7 +89,7 @@ public partial class PowerAccent : IDisposable
_keyboardListener.SetIsLanguageLetterDelegate(new PowerToys.PowerAccentKeyboardService.IsLanguageLetter((LetterKey letterKey, out bool result) =>
{
result = CharacterMappings.GetCharacters(letterKey, _settingService.SelectedLang).Length > 0;
result = Languages.GetDefaultLetterKey(letterKey, _settingService.SelectedLang).Length > 0;
}));
}
@@ -115,7 +115,7 @@ public partial class PowerAccent : IDisposable
private string[] GetCharacters(LetterKey letterKey)
{
var characters = CharacterMappings.GetCharacters(letterKey, _settingService.SelectedLang);
var characters = Languages.GetDefaultLetterKey(letterKey, _settingService.SelectedLang);
if (_settingService.SortByUsageFrequency)
{
characters = characters.OrderByDescending(character => _usageInfo.GetUsageFrequency(character))

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -11,7 +11,6 @@ using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
using PowerAccent.Core.SerializationContext;
using PowerToys.PowerAccentKeyboardService;
using Language = global::PowerAccent.Common.Language;
namespace PowerAccent.Core.Services;

View File

@@ -1,8 +1,7 @@
#pragma once
#pragma once
#include "KeyboardListener.g.h"
#include <mutex>
#include <functional>
#include <spdlog/stopwatch.h>
namespace winrt::PowerToys::PowerAccentKeyboardService::implementation

View File

@@ -1,54 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Models;
namespace PowerDisplay.UnitTests;
[TestClass]
public class BuiltInMonitorBlacklistTests
{
[TestMethod]
public void Entries_LoadsWithoutThrowing()
{
var entries = BuiltInMonitorBlacklist.Entries;
Assert.IsNotNull(entries);
}
[TestMethod]
public void Entries_AreNormalizedToUpperCase()
{
foreach (var entry in BuiltInMonitorBlacklist.Entries)
{
Assert.AreEqual(
entry.EdidId,
entry.EdidId.ToUpperInvariant(),
$"Entry '{entry.EdidId}' is not normalized to uppercase.");
Assert.AreEqual(
entry.EdidId.Trim(),
entry.EdidId,
$"Entry '{entry.EdidId}' has untrimmed whitespace.");
}
}
[TestMethod]
public void Entries_ContainNoEmptyEdidIds()
{
Assert.IsFalse(
BuiltInMonitorBlacklist.Entries.Any(e => string.IsNullOrWhiteSpace(e.EdidId)),
"Built-in list should never contain blank EdidId entries.");
}
[TestMethod]
public void Entries_AreCached()
{
var first = BuiltInMonitorBlacklist.Entries;
var second = BuiltInMonitorBlacklist.Entries;
Assert.AreSame(first, second, "Entries should be returned from a cached Lazy<>.");
}
}

View File

@@ -1,36 +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.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Common.Services;
namespace PowerDisplay.UnitTests;
[TestClass]
public class MonitorBlacklistServiceTests
{
private const string SamplePathDel = @"\\?\DISPLAY#DELD1A8#5&abc123&0&UID12345#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}";
private const string SamplePathBoe = @"\\?\DISPLAY#BOE0900#4&xyz&0&UID0";
[TestMethod]
public void IsBlocked_EmptyBuiltIn_ReturnsFalse()
{
// Built-in list ships empty in this release, so the service should never block.
var service = new MonitorBlacklistService();
Assert.IsFalse(service.IsBlocked(SamplePathDel));
Assert.IsFalse(service.IsBlocked(SamplePathBoe));
}
[TestMethod]
public void IsBlocked_EmptyOrUnknownMonitorId_ReturnsFalse()
{
var service = new MonitorBlacklistService();
Assert.IsFalse(service.IsBlocked(string.Empty));
Assert.IsFalse(service.IsBlocked(null!));
Assert.IsFalse(service.IsBlocked(@"\\?\DISPLAY"));
Assert.IsFalse(service.IsBlocked(@"garbage no hashes here"));
}
}

View File

@@ -703,16 +703,6 @@ namespace PowerDisplay.Common.Drivers.DDC
}
#endif
// Log identity of the monitor we are about to touch via DDC/CI BEFORE the
// first syscall. If the call triggers a kernel stack-cookie overrun inside
// win32kfull (see GH #47556 / #47968), this is the last log line that
// survives — it has to carry enough to identify the offending hardware:
// EdidId for blacklist matching, plus the human-readable name and full
// DevicePath.
var edidId = MonitorIdentity.EdidIdFromMonitorId(info.DevicePath);
Logger.LogInfo(
$"DDC: probing capabilities [EdidId={edidId}] [FriendlyName='{info.FriendlyName}'] [DevicePath={info.DevicePath}]");
// Async caps fetch (retry + max-compat probe). Awaits Task.Delay between
// retries instead of blocking the threadpool.
var (capsString, caps) = await FetchCapabilitiesWithFallbackAsync(

View File

@@ -1,59 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using PowerDisplay.Common.Models;
using PowerDisplay.Models;
namespace PowerDisplay.Common.Services
{
/// <summary>
/// Decides whether a monitor identified by its <c>Monitor.Id</c> (or raw DevicePath)
/// should be filtered out of PowerDisplay's discovery. Matches on EdidId only —
/// model-level granularity, so a single entry covers every physical port and every
/// machine with the same monitor model.
/// </summary>
/// <remarks>
/// Only the built-in list shipped with PowerToys is consulted; user-customized
/// blacklists were considered but cut due to UI cost. EdidIds are normalized
/// (trimmed, upper-cased) on construction; comparisons use
/// <see cref="StringComparer.OrdinalIgnoreCase"/> as defense in depth.
/// </remarks>
public sealed class MonitorBlacklistService
{
private readonly HashSet<string> _blockedEdidIds;
public MonitorBlacklistService()
{
_blockedEdidIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var entry in BuiltInMonitorBlacklist.Entries)
{
AddNormalized(entry.EdidId);
}
}
/// <summary>
/// Returns true if <paramref name="monitorId"/> (a <c>Monitor.Id</c> or raw Windows
/// DevicePath) has an EdidId in the built-in blacklist. Monitors whose EdidId cannot
/// be extracted (empty path, malformed) are never blocked — we only filter what we
/// can positively identify.
/// </summary>
public bool IsBlocked(string monitorId)
{
var edid = MonitorIdentity.EdidIdFromMonitorId(monitorId);
return !string.IsNullOrEmpty(edid) && _blockedEdidIds.Contains(edid);
}
private void AddNormalized(string? edidId)
{
var trimmed = edidId?.Trim();
if (!string.IsNullOrEmpty(trimmed))
{
_blockedEdidIds.Add(trimmed.ToUpperInvariant());
}
}
}
}

View File

@@ -1,76 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
namespace PowerDisplay.Models
{
/// <summary>
/// Loads the built-in monitor blacklist shipped with PowerToys.
/// The data is an embedded JSON resource in this assembly; the file is read once
/// on first access and cached for the lifetime of the process.
/// </summary>
/// <remarks>
/// Loader failures are non-fatal: on any exception (missing resource, malformed
/// JSON, etc.) the loader returns an empty list. This keeps PowerDisplay running
/// even if a malformed release ships, and avoids logging dependencies inside the
/// AOT-compatible PowerDisplay.Models assembly.
/// </remarks>
public static class BuiltInMonitorBlacklist
{
private const string ResourceName = "PowerDisplay.Models.BuiltInMonitorBlacklist.json";
private static readonly Lazy<IReadOnlyList<MonitorBlacklistEntry>> _entries
= new(LoadFromResource);
public static IReadOnlyList<MonitorBlacklistEntry> Entries => _entries.Value;
private static IReadOnlyList<MonitorBlacklistEntry> LoadFromResource()
{
try
{
var assembly = typeof(BuiltInMonitorBlacklist).Assembly;
using var stream = assembly.GetManifestResourceStream(ResourceName);
if (stream == null)
{
return Array.Empty<MonitorBlacklistEntry>();
}
var file = JsonSerializer.Deserialize(
stream,
MonitorBlacklistSerializationContext.Default.BuiltInMonitorBlacklistFile);
if (file?.Entries == null)
{
return Array.Empty<MonitorBlacklistEntry>();
}
// Only the v1 schema is understood by this build. Future versions
// ship a refreshed binary that updates this check.
if (file.Version != 1)
{
return Array.Empty<MonitorBlacklistEntry>();
}
return file.Entries
.Where(e => !string.IsNullOrWhiteSpace(e.EdidId))
.Select(e => new MonitorBlacklistEntry
{
EdidId = e.EdidId.Trim().ToUpperInvariant(),
Comments = e.Comments ?? string.Empty,
})
.ToList();
}
catch
{
return Array.Empty<MonitorBlacklistEntry>();
}
}
}
}

View File

@@ -1,13 +0,0 @@
{
"version": 1,
"entries": [
{
"edidId": "LTM2C02",
"comments": "See https://github.com/microsoft/PowerToys/issues/47556"
},
{
"edidId": "GSM7714",
"comments": "See https://github.com/microsoft/PowerToys/issues/47968"
}
]
}

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 System.Collections.Generic;
using System.Text.Json.Serialization;
namespace PowerDisplay.Models
{
/// <summary>
/// JSON file shape for <see cref="BuiltInMonitorBlacklist"/>.
/// The <see cref="Version"/> field is a forward-compatibility marker; this
/// release only understands version 1.
/// </summary>
public class BuiltInMonitorBlacklistFile
{
[JsonPropertyName("version")]
public int Version { get; set; }
[JsonPropertyName("entries")]
public List<MonitorBlacklistEntry> Entries { get; set; } = new();
}
}

View File

@@ -1,30 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace PowerDisplay.Models
{
/// <summary>
/// One entry in a PowerDisplay monitor blacklist. Used both for the built-in
/// list shipped with PowerToys (loaded via <see cref="BuiltInMonitorBlacklist"/>)
/// and for the user-editable custom list persisted on <c>PowerDisplayProperties</c>.
/// </summary>
/// <remarks>
/// <para><see cref="EdidId"/> is the 78 character PnP hardware identifier extracted
/// from a <c>Monitor.Id</c> by <c>MonitorIdentity.EdidIdFromMonitorId</c> (e.g.
/// <c>"DELD1A8"</c>, <c>"BOE0900"</c>). It is normalized to uppercase and trimmed
/// on write; matching is case-insensitive as a defense-in-depth measure.</para>
/// <para><see cref="Comments"/> is free text rendered as-is. The built-in JSON ships
/// English-only comments; user input is not localized.</para>
/// </remarks>
public class MonitorBlacklistEntry
{
[JsonPropertyName("edidId")]
public string EdidId { get; set; } = string.Empty;
[JsonPropertyName("comments")]
public string Comments { get; set; } = string.Empty;
}
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace PowerDisplay.Models
{
/// <summary>
/// JSON serialization context for monitor blacklist types.
/// Provides source-generated serialization for Native AOT compatibility.
/// </summary>
[JsonSourceGenerationOptions(
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
IncludeFields = true)]
[JsonSerializable(typeof(MonitorBlacklistEntry))]
[JsonSerializable(typeof(List<MonitorBlacklistEntry>))]
[JsonSerializable(typeof(BuiltInMonitorBlacklistFile))]
public partial class MonitorBlacklistSerializationContext : JsonSerializerContext
{
}
}

View File

@@ -17,7 +17,4 @@
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="BuiltInMonitorBlacklist.json" />
</ItemGroup>
</Project>

View File

@@ -31,9 +31,6 @@ namespace PowerDisplay.Helpers
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
private readonly DisplayRotationService _rotationService = new();
// Built-in entries are loaded automatically by the service constructor.
private readonly MonitorBlacklistService _blacklistService = new();
// Controllers stored by type for O(1) lookup based on CommunicationMethod
private DdcCiController? _ddcController;
private WmiController? _wmiController;
@@ -131,34 +128,6 @@ namespace PowerDisplay.Helpers
{
var inventory = DisplayConfigInventory.GetAllMonitorDisplayInfo();
// Filter blacklisted monitors out of the inventory before any controller
// is dispatched. Matching uses MonitorIdentity.EdidIdFromMonitorId on each
// entry's DevicePath, so blocked monitors are not opened, probed, or queried
// — the whole point of the blacklist over the per-monitor IsHidden flag.
var beforeCount = inventory.Count;
var filteredInventory = new Dictionary<string, MonitorDisplayInfo>(
inventory.Count, StringComparer.OrdinalIgnoreCase);
foreach (var kvp in inventory)
{
if (_blacklistService.IsBlocked(kvp.Value.DevicePath))
{
var edidId = MonitorIdentity.EdidIdFromMonitorId(kvp.Value.DevicePath);
Logger.LogInfo(
$"[MonitorBlacklist] Skipping '{kvp.Value.FriendlyName}' (EdidId '{edidId}', path '{kvp.Value.DevicePath}') — EdidId is on the blacklist");
continue;
}
filteredInventory.Add(kvp.Key, kvp.Value);
}
if (filteredInventory.Count < beforeCount)
{
Logger.LogInfo(
$"[MonitorBlacklist] Filtered out {beforeCount - filteredInventory.Count} monitor(s); {filteredInventory.Count} remain");
}
inventory = filteredInventory;
if (inventory.Count == 0)
{
Logger.LogWarning("[MonitorManager] QueryDisplayConfig returned no displays — discovery aborted");

View File

@@ -40,7 +40,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.WindowsAppSDK.Foundation" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Windows.CppWinRT" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Windows.ImplementationLibrary" GeneratePathProperty="true" />
<PackageReference Include="boost" GeneratePathProperty="true" />
@@ -225,12 +224,4 @@
<PRIResource Include="@(_WildCardPRIResource)" />
</ItemGroup>
</Target>
<!-- Deduplicate WindowsAppRuntimeAutoInitializer.cpp (added twice via transitive imports causing MSB8027/LNK4042). Same fix as runner.vcxproj. -->
<Target Name="FixWinAppSDKAutoInitializer" BeforeTargets="ClCompile" AfterTargets="WindowsAppRuntimeAutoInitializer">
<ItemGroup>
<ClCompile Remove="@(ClCompile)" Condition="'%(Filename)' == 'WindowsAppRuntimeAutoInitializer'" />
<ClCompile Include="$(PkgMicrosoft_WindowsAppSDK_Foundation)\include\WindowsAppRuntimeAutoInitializer.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
</ItemGroup>
</Target></Project>
</Project>

View File

@@ -11,7 +11,10 @@ namespace cmdArg
// restarting it from there, so it doesn't interfere with the installation process.
const inline wchar_t* UPDATE_NOW_LAUNCH_STAGE1 = L"-update_now";
// Stage 2 consists of starting the installer and optionally launching newly installed PowerToys binary.
// That's indicated by the following 2 flags.
const inline wchar_t* UPDATE_NOW_LAUNCH_STAGE2 = L"-update_now_stage_2";
const inline wchar_t* UPDATE_STAGE2_RESTART_PT = L"restart";
const inline wchar_t* UPDATE_STAGE2_DONT_START_PT = L"dont_start";
const inline wchar_t* UPDATE_REPORT_SUCCESS = L"-report_update_success";
}

View File

@@ -111,7 +111,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
QuickAccessShortcut = new HotkeySettings();
IsElevated = false;
ShowNewUpdatesToastNotification = true;
AutoDownloadUpdates = true;
AutoDownloadUpdates = false;
EnableExperimentation = true;
DashboardSortOrder = DashboardSortOrder.Alphabetical;
Theme = "system";

View File

@@ -56,7 +56,6 @@ public class SetSettingCommandTests
}
[DataRow(typeof(GeneralSettings), "Enabled.MouseWithoutBorders", "true")]
[DataRow(typeof(GeneralSettings), nameof(GeneralSettings.AutoDownloadUpdates), "false")]
[DataRow(typeof(GeneralSettings), nameof(GeneralSettings.AutoDownloadUpdates), "true")]
[TestMethod]
public void SetGeneralSetting(Type moduleSettingsType, string settingName, string newValueStr)

View File

@@ -115,7 +115,6 @@
<ProjectReference Include="..\..\common\Common.UI.Controls\Common.UI.Controls.csproj" />
<ProjectReference Include="..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\modules\poweraccent\PowerAccent.Common\PowerAccent.Common.csproj" />
<ProjectReference Include="..\..\modules\ZoomIt\ZoomItSettingsInterop\ZoomItSettingsInterop.vcxproj" />
<ProjectReference Include="..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />

View File

@@ -1174,6 +1174,12 @@ opera.exe</value>
<data name="ShortcutGuide_WindowPosition_Right.Content" xml:space="preserve">
<value>Right</value>
</data>
<data name="ShortcutGuide_WindowPosition_Left.Content" xml:space="preserve">
<value>Left</value>
</data>
<data name="ShortcutGuide_WindowPosition_Right.Content" xml:space="preserve">
<value>Right</value>
</data>
<data name="ShortcutGuide_DisabledApps.Header" xml:space="preserve">
<value>Exclude apps</value>
</data>
@@ -2056,6 +2062,9 @@ The Microsoft Windows Operating System
Windows Explorer
Notepad
Microsoft PowerToys
Paint
Microsoft Office Apps
And more...!
The shortcuts always correspond to the most current application/Windows version.</value>
</data>
@@ -5136,7 +5145,7 @@ The break timer font matches the text font.</value>
<data name="UpdateAvailableInfoBar.Title" xml:space="preserve">
<value>Update available</value>
</data>
<data name="GeneralVersion.Text" xml:space="preserve">
<data name="GeneralVersion.Text" xml:space="preserve">
<value>Version</value>
</data>
<data name="LearnWhatsNew.Text" xml:space="preserve">

View File

@@ -12,7 +12,6 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using PowerAccent.Common;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
@@ -24,31 +23,56 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private readonly SettingsUtils _settingsUtils;
/// <summary>
/// Maps each currently supported <see cref="LanguageGroup"/> to its resx
/// resource key so that group header strings can be looked up by the Settings UI.
/// Only groups that already have corresponding Settings UI resources should be
/// listed here.
/// </summary>
private static readonly Dictionary<LanguageGroup, string> _groupResourceKeys = new()
{
[LanguageGroup.Language] = "QuickAccent_Group_Language",
[LanguageGroup.Special] = "QuickAccent_Group_Special",
};
private const string SpecialGroup = "QuickAccent_Group_Special";
private const string LanguageGroup = "QuickAccent_Group_Language";
/// <summary>
/// Gets the flat list of all available languages, derived from
/// <see cref="CharacterMappings.All"/>. In the Settings UI, this list is sorted
/// alphabetically by the localized display name and arranged into groups based on
/// the <see cref="LanguageGroup"/>. Populated by <see cref="InitializeLanguages"/>.
/// </summary>
public List<PowerAccentLanguageModel> Languages { get; private set; }
public List<PowerAccentLanguageModel> Languages { get; } = [
new PowerAccentLanguageModel("SPECIAL", "QuickAccent_SelectedLanguage_Special", SpecialGroup),
new PowerAccentLanguageModel("BG", "QuickAccent_SelectedLanguage_Bulgarian", LanguageGroup),
new PowerAccentLanguageModel("CA", "QuickAccent_SelectedLanguage_Catalan", LanguageGroup),
new PowerAccentLanguageModel("CRH", "QuickAccent_SelectedLanguage_Crimean", LanguageGroup),
new PowerAccentLanguageModel("CUR", "QuickAccent_SelectedLanguage_Currency", SpecialGroup),
new PowerAccentLanguageModel("HR", "QuickAccent_SelectedLanguage_Croatian", LanguageGroup),
new PowerAccentLanguageModel("CZ", "QuickAccent_SelectedLanguage_Czech", LanguageGroup),
new PowerAccentLanguageModel("DK", "QuickAccent_SelectedLanguage_Danish", LanguageGroup),
new PowerAccentLanguageModel("GA", "QuickAccent_SelectedLanguage_Gaeilge", LanguageGroup),
new PowerAccentLanguageModel("GD", "QuickAccent_SelectedLanguage_Gaidhlig", LanguageGroup),
new PowerAccentLanguageModel("NL", "QuickAccent_SelectedLanguage_Dutch", LanguageGroup),
new PowerAccentLanguageModel("EL", "QuickAccent_SelectedLanguage_Greek", LanguageGroup),
new PowerAccentLanguageModel("GRC", "QuickAccent_SelectedLanguage_Greek_Polytonic", LanguageGroup),
new PowerAccentLanguageModel("EST", "QuickAccent_SelectedLanguage_Estonian", LanguageGroup),
new PowerAccentLanguageModel("EPO", "QuickAccent_SelectedLanguage_Esperanto", LanguageGroup),
new PowerAccentLanguageModel("FI", "QuickAccent_SelectedLanguage_Finnish", LanguageGroup),
new PowerAccentLanguageModel("FR", "QuickAccent_SelectedLanguage_French", LanguageGroup),
new PowerAccentLanguageModel("DE", "QuickAccent_SelectedLanguage_German", LanguageGroup),
new PowerAccentLanguageModel("HE", "QuickAccent_SelectedLanguage_Hebrew", LanguageGroup),
new PowerAccentLanguageModel("HU", "QuickAccent_SelectedLanguage_Hungarian", LanguageGroup),
new PowerAccentLanguageModel("IS", "QuickAccent_SelectedLanguage_Icelandic", LanguageGroup),
new PowerAccentLanguageModel("IPA", "QuickAccent_SelectedLanguage_IPA", SpecialGroup),
new PowerAccentLanguageModel("IT", "QuickAccent_SelectedLanguage_Italian", LanguageGroup),
new PowerAccentLanguageModel("KU", "QuickAccent_SelectedLanguage_Kurdish", LanguageGroup),
new PowerAccentLanguageModel("LT", "QuickAccent_SelectedLanguage_Lithuanian", LanguageGroup),
new PowerAccentLanguageModel("MK", "QuickAccent_SelectedLanguage_Macedonian", LanguageGroup),
new PowerAccentLanguageModel("MT", "QuickAccent_SelectedLanguage_Maltese", LanguageGroup),
new PowerAccentLanguageModel("MI", "QuickAccent_SelectedLanguage_Maori", LanguageGroup),
new PowerAccentLanguageModel("NO", "QuickAccent_SelectedLanguage_Norwegian", LanguageGroup),
new PowerAccentLanguageModel("PI", "QuickAccent_SelectedLanguage_Pinyin", LanguageGroup),
new PowerAccentLanguageModel("PIE", "QuickAccent_SelectedLanguage_Proto_Indo_European", LanguageGroup),
new PowerAccentLanguageModel("PL", "QuickAccent_SelectedLanguage_Polish", LanguageGroup),
new PowerAccentLanguageModel("PT", "QuickAccent_SelectedLanguage_Portuguese", LanguageGroup),
new PowerAccentLanguageModel("RO", "QuickAccent_SelectedLanguage_Romanian", LanguageGroup),
new PowerAccentLanguageModel("ROM", "QuickAccent_SelectedLanguage_Romanization", SpecialGroup),
new PowerAccentLanguageModel("SK", "QuickAccent_SelectedLanguage_Slovak", LanguageGroup),
new PowerAccentLanguageModel("SL", "QuickAccent_SelectedLanguage_Slovenian", LanguageGroup),
new PowerAccentLanguageModel("SP", "QuickAccent_SelectedLanguage_Spanish", LanguageGroup),
new PowerAccentLanguageModel("SR", "QuickAccent_SelectedLanguage_Serbian", LanguageGroup),
new PowerAccentLanguageModel("SR_CYRL", "QuickAccent_SelectedLanguage_Serbian_Cyrillic", LanguageGroup),
new PowerAccentLanguageModel("SV", "QuickAccent_SelectedLanguage_Swedish", LanguageGroup),
new PowerAccentLanguageModel("TK", "QuickAccent_SelectedLanguage_Turkish", LanguageGroup),
new PowerAccentLanguageModel("VI", "QuickAccent_SelectedLanguage_Vietnamese", LanguageGroup),
new PowerAccentLanguageModel("CY", "QuickAccent_SelectedLanguage_Welsh", LanguageGroup),
];
/// <summary>
/// Gets the languages arranged into display groups, in the order defined by
/// <see cref="CharacterMappings.GroupDisplayOrder"/>. Bound to the Settings UI
/// list.
/// </summary>
public PowerAccentLanguageGroupModel[] LanguageGroups { get; private set; }
private readonly string[] _toolbarOptions =
@@ -133,50 +157,19 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
/// <summary>
/// Builds the Settings UI language models. This resolves localized display names
/// for each language, sorts by name within each group, and arranges groups in the
/// order defined by <see cref="CharacterMappings.GroupDisplayOrder"/>. The
/// resulting list of languages and groups is stored in the
/// <see cref="Languages"/> and <see cref="LanguageGroups"/> properties, which are
/// bound to the Settings UI.
/// Adds Localized Language Name, sorts by it and splits languages into two groups.
/// </summary>
private void InitializeLanguages()
{
// Build the flat list and resolve localized display names.
Languages = CharacterMappings.All
.Where(lang => _groupResourceKeys.ContainsKey(lang.Group))
.Select(lang =>
foreach (var item in Languages)
{
string languageResourceId = $"QuickAccent_SelectedLanguage_{lang.Identifier}";
item.Language = ResourceLoaderInstance.ResourceLoader.GetString(item.LanguageResourceID);
}
var model = new PowerAccentLanguageModel(
lang.Id.ToString(),
languageResourceId,
_groupResourceKeys[lang.Group]);
model.Language = ResourceLoaderInstance.ResourceLoader.GetString(languageResourceId);
return model;
}).ToList();
// Sort the flat list alphabetically by the localized display name.
Languages.Sort((x, y) => string.Compare(x.Language, y.Language, StringComparison.Ordinal));
// Group them in the explicit order defined by the core library. Note:
// PowerAccentLanguageModel does not hold a direct dependency on the
// LanguageGroup enum. Instead, we use the stable GroupResourceID as a
// decoupled key to map the core groups to the Settings UI models.
LanguageGroups = CharacterMappings.GroupDisplayOrder
.Where(group => _groupResourceKeys.ContainsKey(group))
.Select(group =>
{
string groupResourceId = _groupResourceKeys[group];
var groupedLanguages = Languages.Where(lang => lang.GroupResourceID == groupResourceId).ToList();
return groupedLanguages.Count > 0
? new PowerAccentLanguageGroupModel(groupedLanguages, ResourceLoaderInstance.ResourceLoader.GetString(groupResourceId))
: null; // Skip groups with no languages.
})
.OfType<PowerAccentLanguageGroupModel>()
LanguageGroups = Languages
.GroupBy(language => language.GroupResourceID)
.Select(grp => new PowerAccentLanguageGroupModel(grp.ToList(), ResourceLoaderInstance.ResourceLoader.GetString(grp.Key)))
.ToArray();
}