diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d484b4a395..ccd8140c86 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -46,6 +46,7 @@ body: - PDF Thumbnail - G-code Preview - G-code Thumbnail + - PowerAccent - PowerRename - PowerToys Run - Shortcut Guide diff --git a/.github/ISSUE_TEMPLATE/translation_issue.yml b/.github/ISSUE_TEMPLATE/translation_issue.yml index 326ceaacba..8b78313bb4 100644 --- a/.github/ISSUE_TEMPLATE/translation_issue.yml +++ b/.github/ISSUE_TEMPLATE/translation_issue.yml @@ -37,6 +37,7 @@ body: - PDF Thumbnail - G-code Preview - G-code Thumbnail + - PowerAccent - PowerRename - PowerToys Run - Shortcut Guide diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index b0da81404b..a1c204c873 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -201,10 +201,13 @@ BYPOSITION bytearray Caiguna CALG +Calibri callbackptr Cangjie CANRENAME +coord CAPTURECHANGED +carret cassert CAtl cch @@ -379,6 +382,7 @@ CYVIRTUALSCREEN cziplib Dac dacl +damienleroy Danmarkshavn DARKPURPLE DARKTEAL @@ -472,6 +476,7 @@ DPolicy DPSAPI DQTAT DQTYPE +dragdrop DRAWFRAME drawingcolor dreamsofameaningfullife @@ -488,6 +493,7 @@ DVSD DVSL DVTARGETDEVICE DWINRT +dwhkl dwl dwm dwmapi @@ -678,6 +684,7 @@ HACCEL hangeul hanselman hardcoded +hardcodet Hardlines HARDWAREINPUT hashcode @@ -718,12 +725,13 @@ HIMAGELIST himl hinst hinstance +hitted HIWORD HKCC HKCR HKCU hkey -HKL +hkl HKLM HKPD HKU @@ -879,6 +887,7 @@ inheritdoc initguid Inkscape Inlines +Inlining inorder INotification INotify @@ -1008,6 +1017,7 @@ jxr jyuwono KBDLLHOOKSTRUCT kbm +KCode KEYBDINPUT keybindings keyboardeventhandlers @@ -1102,6 +1112,7 @@ LOCATIONCHANGE logconsole logfile LOGFONT +Logique LOGMSG logon LOGPIXELSX @@ -1153,6 +1164,8 @@ lzw Maarten Macquarie Magadan +mah +mahapps Mainwindow majortype MAJORVERSION @@ -1209,6 +1222,7 @@ mfplat Mfsensorgroup mftransform mic +michkap microsoft Midl mii @@ -1531,6 +1545,7 @@ popup POPUPWINDOW posix powercfg +poweraccent powerlauncher POWEROCR powerpreview @@ -2163,6 +2178,7 @@ uwp uxtheme UYVY validmodulename +Vanara vcamp vcdl VCINSTALLDIR @@ -2188,11 +2204,14 @@ VIDEOINFOHEADER viewbox viewmodel vih +Virt virtualization +Virtualizing visiblecolorformats Visibletrue visualbrush visualstudio +viter VKey VKTAB VOS diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index 35732769d7..cd26688338 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -106,6 +106,13 @@ "modules\\MouseUtils\\PowerToys.MouseHighlighter.dll", "modules\\MouseUtils\\PowerToys.MousePointerCrosshairs.dll", + "modules\\PowerAccent\\PowerAccent.Core.dll", + "modules\\PowerAccent\\PowerAccent.dll", + "modules\\PowerAccent\\PowerAccent.exe", + "modules\\PowerAccent\\PowerToys.PowerAccent.dll", + "modules\\PowerAccent\\PowerToys.PowerAccent.exe", + "modules\\PowerAccent\\PowerToys.PowerAccentModuleInterface.dll", + "modules\\PowerRename\\PowerToys.PowerRenameExt.dll", "modules\\PowerRename\\PowerToys.PowerRename.exe", "modules\\PowerRename\\PowerToys.PowerRenameContextMenu.dll", @@ -187,7 +194,12 @@ "vccorlib140_app.dll", "vcomp140_app.dll", "vcruntime140_1_app.dll", - "vcruntime140_app.dll", + "vcruntime140_app.dll", + "modules\\PowerAccent\\Vanara.Core.dll", + "modules\\PowerAccent\\Vanara.PInvoke.Gdi32.dll", + "modules\\PowerAccent\\Vanara.PInvoke.Kernel32.dll", + "modules\\PowerAccent\\Vanara.PInvoke.Shared.dll", + "modules\\PowerAccent\\Vanara.PInvoke.User32.dll", "modules\\FileExplorerPreview\\Microsoft.Web.WebView2.Core.dll", "modules\\FileExplorerPreview\\Microsoft.Web.WebView2.WinForms.dll", "modules\\FileExplorerPreview\\Microsoft.Web.WebView2.Wpf.dll", diff --git a/PowerToys.sln b/PowerToys.sln index ce7badf67d..362efea911 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -407,7 +407,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameUI", "src\module EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameContextMenu", "src\modules\powerrename\PowerRenameContextMenu\PowerRenameContextMenu.vcxproj", "{1DBBB112-4BB1-444B-8EBB-E66555C76BA6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.PowerToys.Run.Plugin.TimeZone.UnitTests", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.TimeZone.UnitTests\Microsoft.PowerToys.Run.Plugin.TimeZone.UnitTests.csproj", "{C5D46169-5334-48C3-8C28-644C72832E54}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.TimeZone.UnitTests", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.TimeZone.UnitTests\Microsoft.PowerToys.Run.Plugin.TimeZone.UnitTests.csproj", "{C5D46169-5334-48C3-8C28-644C72832E54}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.OneNote", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.OneNote\Microsoft.PowerToys.Run.Plugin.OneNote.csproj", "{5A1DB2F0-0715-4B3B-98E6-79BC41540045}" EndProject @@ -415,6 +415,16 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ImageResizerContextMenu", " EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ImageResizerLib", "src\modules\imageresizer\ImageResizerLib\ImageResizerLib.vcxproj", "{18B3DB45-4FFE-4D01-97D6-5223FEEE1853}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PowerAccent", "PowerAccent", "{0F14491C-6369-4C45-AAA8-135814E66E6B}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerAccentModuleInterface", "src\modules\poweraccent\PowerAccentModuleInterface\PowerAccentModuleInterface.vcxproj", "{34A354C5-23C7-4343-916C-C52DAF4FC39D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerAccent", "src\modules\poweraccent\PowerAccent\PowerAccent.csproj", "{7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerAccent.Core", "src\modules\poweraccent\PowerAccent.Core\PowerAccent.Core.csproj", "{3264DF53-C805-4B0C-867C-FCEAF7AEF762}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerAccent.UI", "src\modules\poweraccent\PowerAccent.UI\PowerAccent.UI.csproj", "{31CAD28E-778A-441C-85BC-40AB3EAA2A10}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PowerOCR", "PowerOCR", "{A50C70A6-2DA0-4027-B90E-B1A40755A8A5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerOCR", "src\modules\PowerOCR\PowerOCR\PowerOCR.csproj", "{25C91A4E-BA4E-467A-85CD-8B62545BF674}" @@ -1640,6 +1650,54 @@ Global {18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Release|x64.Build.0 = Release|x64 {18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Release|x86.ActiveCfg = Release|x64 {18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Release|x86.Build.0 = Release|x64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Debug|ARM64.Build.0 = Debug|ARM64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Debug|x64.ActiveCfg = Debug|x64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Debug|x64.Build.0 = Debug|x64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Debug|x86.ActiveCfg = Debug|x64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Debug|x86.Build.0 = Debug|x64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Release|ARM64.ActiveCfg = Release|ARM64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Release|ARM64.Build.0 = Release|ARM64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Release|x64.ActiveCfg = Release|x64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Release|x64.Build.0 = Release|x64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Release|x86.ActiveCfg = Release|x64 + {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Release|x86.Build.0 = Release|x64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Debug|ARM64.Build.0 = Debug|ARM64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Debug|x64.ActiveCfg = Debug|x64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Debug|x64.Build.0 = Debug|x64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Debug|x86.ActiveCfg = Debug|x64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Debug|x86.Build.0 = Debug|x64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Release|ARM64.ActiveCfg = Release|ARM64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Release|ARM64.Build.0 = Release|ARM64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Release|x64.ActiveCfg = Release|x64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Release|x64.Build.0 = Release|x64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Release|x86.ActiveCfg = Release|x64 + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C}.Release|x86.Build.0 = Release|x64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Debug|ARM64.Build.0 = Debug|ARM64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Debug|x64.ActiveCfg = Debug|x64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Debug|x64.Build.0 = Debug|x64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Debug|x86.ActiveCfg = Debug|x64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Debug|x86.Build.0 = Debug|x64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Release|ARM64.ActiveCfg = Release|ARM64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Release|ARM64.Build.0 = Release|ARM64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Release|x64.ActiveCfg = Release|x64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Release|x64.Build.0 = Release|x64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Release|x86.ActiveCfg = Release|x64 + {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Release|x86.Build.0 = Release|x64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Debug|ARM64.Build.0 = Debug|ARM64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Debug|x64.ActiveCfg = Debug|x64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Debug|x64.Build.0 = Debug|x64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Debug|x86.ActiveCfg = Debug|x64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Debug|x86.Build.0 = Debug|x64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Release|ARM64.ActiveCfg = Release|ARM64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Release|ARM64.Build.0 = Release|ARM64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Release|x64.ActiveCfg = Release|x64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Release|x64.Build.0 = Release|x64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Release|x86.ActiveCfg = Release|x64 + {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Release|x86.Build.0 = Release|x64 {25C91A4E-BA4E-467A-85CD-8B62545BF674}.Debug|ARM64.ActiveCfg = Debug|ARM64 {25C91A4E-BA4E-467A-85CD-8B62545BF674}.Debug|ARM64.Build.0 = Debug|ARM64 {25C91A4E-BA4E-467A-85CD-8B62545BF674}.Debug|x64.ActiveCfg = Debug|x64 @@ -1814,6 +1872,11 @@ Global {5A1DB2F0-0715-4B3B-98E6-79BC41540045} = {4AFC9975-2456-4C70-94A4-84073C1CED93} {93B72A06-C8BD-484F-A6F7-C9F280B150BF} = {6C7F47CC-2151-44A3-A546-41C70025132C} {18B3DB45-4FFE-4D01-97D6-5223FEEE1853} = {6C7F47CC-2151-44A3-A546-41C70025132C} + {0F14491C-6369-4C45-AAA8-135814E66E6B} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} + {34A354C5-23C7-4343-916C-C52DAF4FC39D} = {0F14491C-6369-4C45-AAA8-135814E66E6B} + {7B4CDB0D-28C9-4F95-88AA-73FCC06E354C} = {0F14491C-6369-4C45-AAA8-135814E66E6B} + {3264DF53-C805-4B0C-867C-FCEAF7AEF762} = {0F14491C-6369-4C45-AAA8-135814E66E6B} + {31CAD28E-778A-441C-85BC-40AB3EAA2A10} = {0F14491C-6369-4C45-AAA8-135814E66E6B} {A50C70A6-2DA0-4027-B90E-B1A40755A8A5} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} {25C91A4E-BA4E-467A-85CD-8B62545BF674} = {A50C70A6-2DA0-4027-B90E-B1A40755A8A5} {6AB6A2D6-F859-4A82-9184-0BD29C9F07D1} = {A50C70A6-2DA0-4027-B90E-B1A40755A8A5} diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 985cee071f..f98c636c51 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -6,6 +6,7 @@ + @@ -49,11 +50,11 @@ - + - + - + @@ -119,6 +120,8 @@ + + + + @@ -754,6 +759,15 @@ + + + + + + + + + @@ -1054,6 +1068,9 @@ + + + diff --git a/src/common/interop/interop.cpp b/src/common/interop/interop.cpp index a754874208..06afabd1c5 100644 --- a/src/common/interop/interop.cpp +++ b/src/common/interop/interop.cpp @@ -203,5 +203,9 @@ public return gcnew String(CommonSharedConstants::AWAKE_EXIT_EVENT); } + static String ^ PowerAccentExitEvent() { + return gcnew String(CommonSharedConstants::POWERACCENT_EXIT_EVENT); + } + }; } diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index b7c70a690d..7adf617047 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -38,6 +38,9 @@ namespace CommonSharedConstants // Path to the event used by AlwaysOnTop const wchar_t ALWAYS_ON_TOP_PIN_EVENT[] = L"Local\\AlwaysOnTopPinEvent-892e0aa2-cfa8-4cc4-b196-ddeb32314ce8"; + // Path to the event used by PowerAccent + const wchar_t POWERACCENT_EXIT_EVENT[] = L"Local\\PowerToysPowerAccentExitEvent-53e93389-d19a-4fbb-9b36-1981c8965e17"; + // Path to the event used by PowerOCR const wchar_t SHOW_POWEROCR_SHARED_EVENT[] = L"Local\\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a"; diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index d368228a9e..08b02438c4 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -18,6 +18,7 @@ struct LogSettings inline const static std::string launcherLoggerName = "launcher"; inline const static std::wstring launcherLogPath = L"LogsModuleInterface\\launcher-log.txt"; inline const static std::wstring awakeLogPath = L"Logs\\awake-log.txt"; + inline const static std::wstring powerAccentLogPath = L"poweraccent.log"; inline const static std::string fancyZonesLoggerName = "fancyzones"; inline const static std::wstring fancyZonesLogPath = L"fancyzones-log.txt"; inline const static std::wstring fancyZonesOldLogPath = L"FancyZonesLogs\\"; // needed to clean up old logs diff --git a/src/modules/poweraccent/PowerAccent.Core/Models/KeysEnums.cs b/src/modules/poweraccent/PowerAccent.Core/Models/KeysEnums.cs new file mode 100644 index 0000000000..0e6aa7df58 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/Models/KeysEnums.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Vanara.PInvoke; + +namespace PowerAccent.Core; + +public enum LetterKey +{ + A = User32.VK.VK_A, + C = User32.VK.VK_C, + E = User32.VK.VK_E, + I = User32.VK.VK_I, + N = User32.VK.VK_N, + O = User32.VK.VK_O, + S = User32.VK.VK_S, + U = User32.VK.VK_U, + Y = User32.VK.VK_Y, +} + +public enum TriggerKey +{ + Left = User32.VK.VK_LEFT, + Right = User32.VK.VK_RIGHT, + Space = User32.VK.VK_SPACE, +} diff --git a/src/modules/poweraccent/PowerAccent.Core/Models/Point.cs b/src/modules/poweraccent/PowerAccent.Core/Models/Point.cs new file mode 100644 index 0000000000..d58d305a2f --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/Models/Point.cs @@ -0,0 +1,58 @@ +// 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.Core; + +public struct Point +{ + public Point() + { + X = 0; + Y = 0; + } + + public Point(double x, double y) + { + X = x; + Y = y; + } + + public Point(int x, int y) + { + X = x; + Y = y; + } + + public Point(System.Drawing.Point point) + { + X = point.X; + Y = point.Y; + } + + public double X { get; init; } + + public double Y { get; init; } + + public static implicit operator Point(System.Drawing.Point point) => new Point(point.X, point.Y); + + public static Point operator /(Point point, double divider) + { + if (divider == 0) + { + throw new DivideByZeroException(); + } + + return new Point(point.X / divider, point.Y / divider); + } + + public static Point operator /(Point point, Point divider) + { + if (divider.X == 0 || divider.Y == 0) + { + throw new DivideByZeroException(); + } + + return new Point(point.X / divider.X, point.Y / divider.Y); + } +} diff --git a/src/modules/poweraccent/PowerAccent.Core/Models/Rect.cs b/src/modules/poweraccent/PowerAccent.Core/Models/Rect.cs new file mode 100644 index 0000000000..fddc8235b5 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/Models/Rect.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace PowerAccent.Core; + +public struct Rect +{ + public Rect() + { + X = 0; + Y = 0; + Width = 0; + Height = 0; + } + + public Rect(int x, int y, int width, int height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + + public Rect(double x, double y, double width, double height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + + public Rect(Point coord, Size size) + { + X = coord.X; + Y = coord.Y; + Width = size.Width; + Height = size.Height; + } + + public double X { get; init; } + + public double Y { get; init; } + + public double Width { get; init; } + + public double Height { get; init; } + + public static Rect operator /(Rect rect, double divider) + { + if (divider == 0) + { + throw new DivideByZeroException(); + } + + return new Rect(rect.X / divider, rect.Y / divider, rect.Width / divider, rect.Height / divider); + } + + public static Rect operator /(Rect rect, Rect divider) + { + if (divider.X == 0 || divider.Y == 0) + { + throw new DivideByZeroException(); + } + + return new Rect(rect.X / divider.X, rect.Y / divider.Y, rect.Width / divider.Width, rect.Height / divider.Height); + } +} diff --git a/src/modules/poweraccent/PowerAccent.Core/Models/Size.cs b/src/modules/poweraccent/PowerAccent.Core/Models/Size.cs new file mode 100644 index 0000000000..af1b553eef --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/Models/Size.cs @@ -0,0 +1,52 @@ +// 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.Core; + +public struct Size +{ + public Size() + { + Width = 0; + Height = 0; + } + + public Size(double width, double height) + { + Width = width; + Height = height; + } + + public Size(int width, int height) + { + Width = width; + Height = height; + } + + public double Width { get; init; } + + public double Height { get; init; } + + public static implicit operator Size(System.Drawing.Size size) => new Size(size.Width, size.Height); + + public static Size operator /(Size size, double divider) + { + if (divider == 0) + { + throw new DivideByZeroException(); + } + + return new Size(size.Width / divider, size.Height / divider); + } + + public static Size operator /(Size size, Size divider) + { + if (divider.Width == 0 || divider.Height == 0 || divider.Width == 0 || divider.Height == 0) + { + throw new DivideByZeroException(); + } + + return new Size(size.Width / divider.Width, size.Height / divider.Height); + } +} diff --git a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj new file mode 100644 index 0000000000..6921351589 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj @@ -0,0 +1,20 @@ + + + + net6.0-windows + win-x64;win-arm64 + enable + disable + True + + + + + + + + + + + + diff --git a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs new file mode 100644 index 0000000000..959c6f1092 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using Microsoft.PowerToys.Settings.UI.Library.Enumerations; +using PowerAccent.Core.Services; +using PowerAccent.Core.Tools; + +namespace PowerAccent.Core; + +public class PowerAccent : IDisposable +{ + private readonly SettingsService _settingService = new SettingsService(); + private readonly KeyboardListener _keyboardListener = new KeyboardListener(); + + private LetterKey? letterPressed; + private bool _visible; + private char[] _characters = Array.Empty(); + private int _selectedIndex = -1; + private Stopwatch _stopWatch; + private bool _triggeredWithSpace; + + public event Action OnChangeDisplay; + + public event Action OnSelectCharacter; + + public PowerAccent() + { + _keyboardListener.KeyDown += PowerAccent_KeyDown; + _keyboardListener.KeyUp += PowerAccent_KeyUp; + } + + private bool PowerAccent_KeyDown(object sender, KeyboardListener.RawKeyEventArgs args) + { + if (Enum.IsDefined(typeof(LetterKey), (int)args.Key)) + { + _stopWatch = Stopwatch.StartNew(); + letterPressed = (LetterKey)args.Key; + } + + TriggerKey? triggerPressed = null; + if (letterPressed.HasValue) + { + if (Enum.IsDefined(typeof(TriggerKey), (int)args.Key)) + { + triggerPressed = (TriggerKey)args.Key; + + if ((triggerPressed == TriggerKey.Space && _settingService.ActivationKey == PowerAccentActivationKey.LeftRightArrow) || + ((triggerPressed == TriggerKey.Left || triggerPressed == TriggerKey.Right) && _settingService.ActivationKey == PowerAccentActivationKey.Space)) + { + triggerPressed = null; + } + } + } + + if (!_visible && letterPressed.HasValue && triggerPressed.HasValue) + { + // Keep track if it was triggered with space so that it can be typed on false starts. + _triggeredWithSpace = triggerPressed.Value == TriggerKey.Space; + _visible = true; + _characters = WindowsFunctions.IsCapitalState() ? ToUpper(_settingService.GetLetterKey(letterPressed.Value)) : _settingService.GetLetterKey(letterPressed.Value); + Task.Delay(_settingService.InputTime).ContinueWith( + t => + { + if (_visible) + { + OnChangeDisplay?.Invoke(true, _characters); + } + }, TaskScheduler.FromCurrentSynchronizationContext()); + } + + if (_visible && triggerPressed.HasValue) + { + if (_selectedIndex == -1) + { + if (triggerPressed.Value == TriggerKey.Left) + { + _selectedIndex = (_characters.Length / 2) - 1; + } + + if (triggerPressed.Value == TriggerKey.Right) + { + _selectedIndex = _characters.Length / 2; + } + + if (triggerPressed.Value == TriggerKey.Space) + { + _selectedIndex = 0; + } + + if (_selectedIndex < 0) + { + _selectedIndex = 0; + } + + if (_selectedIndex > _characters.Length - 1) + { + _selectedIndex = _characters.Length - 1; + } + + OnSelectCharacter?.Invoke(_selectedIndex, _characters[_selectedIndex]); + return false; + } + + if (triggerPressed.Value == TriggerKey.Space) + { + if (_selectedIndex < _characters.Length - 1) + { + ++_selectedIndex; + } + else + { + _selectedIndex = 0; + } + } + + if (triggerPressed.Value == TriggerKey.Left && _selectedIndex > 0) + { + --_selectedIndex; + } + + if (triggerPressed.Value == TriggerKey.Right && _selectedIndex < _characters.Length - 1) + { + ++_selectedIndex; + } + + OnSelectCharacter?.Invoke(_selectedIndex, _characters[_selectedIndex]); + return false; + } + + return true; + } + + private bool PowerAccent_KeyUp(object sender, KeyboardListener.RawKeyEventArgs args) + { + if (Enum.IsDefined(typeof(LetterKey), (int)args.Key)) + { + letterPressed = null; + _stopWatch.Stop(); + if (_visible) + { + if (_stopWatch.ElapsedMilliseconds < _settingService.InputTime) + { + /* Debug.WriteLine("Insert before inputTime - " + _stopWatch.ElapsedMilliseconds); */ + + // False start, we should output the space if it was the trigger. + if (_triggeredWithSpace) + { + WindowsFunctions.Insert(' '); + } + + OnChangeDisplay?.Invoke(false, null); + _selectedIndex = -1; + _visible = false; + return false; + } + + /* Debug.WriteLine("Insert after inputTime - " + _stopWatch.ElapsedMilliseconds); */ + OnChangeDisplay?.Invoke(false, null); + if (_selectedIndex != -1) + { + WindowsFunctions.Insert(_characters[_selectedIndex], true); + } + + _selectedIndex = -1; + _visible = false; + } + } + + return true; + } + + public Point GetDisplayCoordinates(Size window) + { + var activeDisplay = WindowsFunctions.GetActiveDisplay(); + Rect screen = new Rect(activeDisplay.Location, activeDisplay.Size) / activeDisplay.Dpi; + Position position = _settingService.Position; + + /* Debug.WriteLine("Dpi: " + activeDisplay.Dpi); */ + + return Calculation.GetRawCoordinatesFromPosition(position, screen, window); + } + + public char[] GetLettersFromKey(LetterKey letter) + { + return _settingService.GetLetterKey(letter); + } + + public void Dispose() + { + _keyboardListener.Dispose(); + GC.SuppressFinalize(this); + } + + public static char[] ToUpper(char[] array) + { + char[] result = new char[array.Length]; + for (int i = 0; i < array.Length; i++) + { + result[i] = char.ToUpper(array[i], System.Globalization.CultureInfo.InvariantCulture); + } + + return result; + } +} diff --git a/src/modules/poweraccent/PowerAccent.Core/Services/SettingsService.cs b/src/modules/poweraccent/PowerAccent.Core/Services/SettingsService.cs new file mode 100644 index 0000000000..9875324f34 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/Services/SettingsService.cs @@ -0,0 +1,180 @@ +// 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.Core.Services; + +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Enumerations; +using Microsoft.PowerToys.Settings.UI.Library.Utilities; +using System.IO.Abstractions; +using System.Text.Json; + +public class SettingsService +{ + private const string PowerAccentModuleName = "PowerAccent"; + private readonly ISettingsUtils _settingsUtils; + private readonly IFileSystemWatcher _watcher; + private readonly object _loadingSettingsLock = new object(); + + public SettingsService() + { + _settingsUtils = new SettingsUtils(); + ReadSettings(); + _watcher = Helper.GetFileWatcher(PowerAccentModuleName, "settings.json", () => { ReadSettings(); }); + } + + private void ReadSettings() + { + // TODO this IO call should by Async, update GetFileWatcher helper to support async + lock (_loadingSettingsLock) + { + { + try + { + if (!_settingsUtils.SettingsExists(PowerAccentModuleName)) + { + Logger.LogInfo("PowerAccent settings.json was missing, creating a new one"); + var defaultSettings = new PowerAccentSettings(); + var options = new JsonSerializerOptions + { + WriteIndented = true, + }; + + _settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), PowerAccentModuleName); + } + + var settings = _settingsUtils.GetSettingsOrDefault(PowerAccentModuleName); + if (settings != null) + { + ActivationKey = settings.Properties.ActivationKey; + switch (settings.Properties.ToolbarPosition.Value) + { + case "Top center": + Position = Position.Top; + break; + case "Bottom center": + Position = Position.Bottom; + break; + case "Left": + Position = Position.Left; + break; + case "Right": + Position = Position.Right; + break; + case "Top right corner": + Position = Position.TopRight; + break; + case "Top left corner": + Position = Position.TopLeft; + break; + case "Bottom right corner": + Position = Position.BottomRight; + break; + case "Bottom left corner": + Position = Position.BottomLeft; + break; + case "Center": + Position = Position.Center; + break; + } + } + } + catch (Exception ex) + { + Logger.LogError("Failed to read changed settings", ex); + } + } + } + } + + private PowerAccentActivationKey _activationKey = PowerAccentActivationKey.Both; + + public PowerAccentActivationKey ActivationKey + { + get + { + return _activationKey; + } + + set + { + _activationKey = value; + } + } + + private Position _position = Position.Top; + + public Position Position + { + get + { + return _position; + } + + set + { + _position = value; + } + } + + private int _inputTime = 200; + + public int InputTime + { + get + { + return _inputTime; + } + + set + { + _inputTime = value; + } + } + + public char[] GetLetterKey(LetterKey letter) + { + return GetDefaultLetterKey(letter); + } + + public static char[] GetDefaultLetterKey(LetterKey letter) + { + switch (letter) + { + case LetterKey.A: + return new char[] { 'à', 'â', 'á', 'ä', 'ã', 'å', 'æ' }; + case LetterKey.C: + return new char[] { 'ć', 'ĉ', 'č', 'ċ', 'ç', 'ḉ' }; + case LetterKey.E: + return new char[] { 'é', 'è', 'ê', 'ë', 'ē', 'ė', '€' }; + case LetterKey.I: + return new char[] { 'î', 'ï', 'í', 'ì', 'ī' }; + case LetterKey.N: + return new char[] { 'ñ', 'ń' }; + case LetterKey.O: + return new char[] { 'ô', 'ö', 'ó', 'ò', 'õ', 'ø', 'œ' }; + case LetterKey.S: + return new char[] { 'š', 'ß', 'ś' }; + case LetterKey.U: + return new char[] { 'û', 'ù', 'ü', 'ú', 'ū' }; + case LetterKey.Y: + return new char[] { 'ÿ', 'ý' }; + } + + throw new ArgumentException("Letter {0} is missing", letter.ToString()); + } +} + +public enum Position +{ + Top, + Bottom, + Left, + Right, + TopLeft, + TopRight, + BottomLeft, + BottomRight, + Center, +} diff --git a/src/modules/poweraccent/PowerAccent.Core/Telemetry/PowerAccentShowAccentMenuEvent.cs b/src/modules/poweraccent/PowerAccent.Core/Telemetry/PowerAccentShowAccentMenuEvent.cs new file mode 100644 index 0000000000..37ff454258 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/Telemetry/PowerAccentShowAccentMenuEvent.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace PowerAccent.Core.Telemetry +{ + [EventData] + public class PowerAccentShowAccentMenuEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +} diff --git a/src/modules/poweraccent/PowerAccent.Core/Tools/Calculation.cs b/src/modules/poweraccent/PowerAccent.Core/Tools/Calculation.cs new file mode 100644 index 0000000000..e3b23ec73a --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/Tools/Calculation.cs @@ -0,0 +1,50 @@ +// 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 PowerAccent.Core.Services; + +namespace PowerAccent.Core.Tools +{ + internal static class Calculation + { + public static Point GetRawCoordinatesFromCaret(Point caret, Rect screen, Size window) + { + var left = caret.X - (window.Width / 2); + var top = caret.Y - window.Height - 20; + + return new Point( + left < screen.X ? screen.X : (left + window.Width > (screen.X + screen.Width) ? (screen.X + screen.Width) - window.Width : left), + top < screen.Y ? caret.Y + 20 : top); + } + + public static Point GetRawCoordinatesFromPosition(Position position, Rect screen, Size window) + { + int offset = 10; + + double pointX = position switch + { + Position.Top or Position.Bottom or Position.Center + => screen.X + (screen.Width / 2) - (window.Width / 2), + Position.TopLeft or Position.Left or Position.BottomLeft + => screen.X + offset, + Position.TopRight or Position.Right or Position.BottomRight + => screen.X + screen.Width - (window.Width + offset), + _ => throw new NotImplementedException(), + }; + + double pointY = position switch + { + Position.TopLeft or Position.Top or Position.TopRight + => screen.Y + offset, + Position.Left or Position.Center or Position.Right + => screen.Y + (screen.Height / 2) - (window.Height / 2), + Position.BottomLeft or Position.Bottom or Position.BottomRight + => screen.Y + screen.Height - (window.Height + offset), + _ => throw new NotImplementedException(), + }; + + return new Point(pointX, pointY); + } + } +} diff --git a/src/modules/poweraccent/PowerAccent.Core/Tools/KeyboardListener.cs b/src/modules/poweraccent/PowerAccent.Core/Tools/KeyboardListener.cs new file mode 100644 index 0000000000..f993dc1214 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/Tools/KeyboardListener.cs @@ -0,0 +1,359 @@ +// 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 warning disable SA1310 // FieldNamesMustNotContainUnderscore + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace PowerAccent.Core.Tools; + +internal class KeyboardListener : IDisposable +{ + /// + /// Initializes a new instance of the class. + /// Creates global keyboard listener. + /// + public KeyboardListener() + { + // We have to store the LowLevelKeyboardProc, so that it is not garbage collected by runtime + _hookedLowLevelKeyboardProc = LowLevelKeyboardProc; + + // Set the hook + _hookId = InterceptKeys.SetHook(_hookedLowLevelKeyboardProc); + + // Assign the asynchronous callback event + hookedKeyboardCallbackAsync = new KeyboardCallbackAsync(KeyboardListener_KeyboardCallbackAsync); + } + + /// + /// Fired when any of the keys is pressed down. + /// + public event RawKeyEventHandler KeyDown; + + /// + /// Fired when any of the keys is released. + /// + public event RawKeyEventHandler KeyUp; + + /// + /// Hook ID + /// + private readonly IntPtr _hookId = IntPtr.Zero; + + /// + /// Contains the hooked callback in runtime. + /// + private readonly InterceptKeys.LowLevelKeyboardProc _hookedLowLevelKeyboardProc; + + /// + /// Event to be invoked asynchronously (BeginInvoke) each time key is pressed. + /// + private KeyboardCallbackAsync hookedKeyboardCallbackAsync; + + /// + /// Raw keyevent handler. + /// + /// sender + /// raw keyevent arguments + public delegate bool RawKeyEventHandler(object sender, RawKeyEventArgs args); + + /// + /// Asynchronous callback hook. + /// + /// Keyboard event + /// VKCode + /// Character + private delegate bool KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode, string character); + + /// + /// Actual callback hook. + /// Calls asynchronously the asyncCallback. + /// + /// VKCode + /// wParam + /// lParam + [MethodImpl(MethodImplOptions.NoInlining)] + private IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam) + { + if (nCode >= 0) + { + if (wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYDOWN || + wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYUP) + { + // Captures the character(s) pressed only on WM_KEYDOWN + var chars = InterceptKeys.VKCodeToString( + (uint)Marshal.ReadInt32(lParam), + wParam.ToUInt32() == (int)InterceptKeys.KeyEvent.WM_KEYDOWN); + + if (!hookedKeyboardCallbackAsync.Invoke((InterceptKeys.KeyEvent)wParam.ToUInt32(), Marshal.ReadInt32(lParam), chars)) + { + return (IntPtr)1; + } + } + } + + return InterceptKeys.CallNextHookEx(_hookId, nCode, wParam, lParam); + } + + /// + /// HookCallbackAsync procedure that calls accordingly the KeyDown or KeyUp events. + /// + /// Keyboard event + /// VKCode + /// Character as string. + private bool KeyboardListener_KeyboardCallbackAsync(InterceptKeys.KeyEvent keyEvent, int vkCode, string character) + { + switch (keyEvent) + { + // KeyDown events + case InterceptKeys.KeyEvent.WM_KEYDOWN: + if (KeyDown != null) + { + return KeyDown.Invoke(this, new RawKeyEventArgs(vkCode, character)); + } + + break; + + // KeyUp events + case InterceptKeys.KeyEvent.WM_KEYUP: + if (KeyUp != null) + { + return KeyUp.Invoke(this, new RawKeyEventArgs(vkCode, character)); + } + + break; + default: + break; + } + + return true; + } + + public void Dispose() + { + InterceptKeys.UnhookWindowsHookEx(_hookId); + } + + /// + /// Raw KeyEvent arguments. + /// + public class RawKeyEventArgs : EventArgs + { + /// + /// WPF Key of the key. + /// +#pragma warning disable SA1401 // Fields should be private + public uint Key; +#pragma warning restore SA1401 // Fields should be private + + /// + /// Convert to string. + /// + /// Returns string representation of this key, if not possible empty string is returned. + public override string ToString() + { + return character; + } + + /// + /// Unicode character of key pressed. + /// + private string character; + + /// + /// Initializes a new instance of the class. + /// Create raw keyevent arguments. + /// + /// VKCode + /// Character + public RawKeyEventArgs(int vKCode, string character) + { + this.character = character; + Key = (uint)vKCode; // User32.MapVirtualKey((uint)VKCode, User32.MAPVK.MAPVK_VK_TO_VSC_EX); + } + } +} + +/// +/// Winapi Key interception helper class. +/// +internal static class InterceptKeys +{ + public delegate IntPtr LowLevelKeyboardProc(int nCode, UIntPtr wParam, IntPtr lParam); + + private const int WH_KEYBOARD_LL = 13; + + /// + /// Key event + /// + public enum KeyEvent : int + { + /// + /// Key down + /// + WM_KEYDOWN = 256, + + /// + /// Key up + /// + WM_KEYUP = 257, + + /// + /// System key up + /// + WM_SYSKEYUP = 261, + + /// + /// System key down + /// + WM_SYSKEYDOWN = 260, + } + + public static IntPtr SetHook(LowLevelKeyboardProc proc) + { + using (Process curProcess = Process.GetCurrentProcess()) + using (ProcessModule curModule = curProcess.MainModule) + { + return SetWindowsHookEx(WH_KEYBOARD_LL, proc, (IntPtr)0, 0); + } + } + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool UnhookWindowsHookEx(IntPtr hhk); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, UIntPtr wParam, IntPtr lParam); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern IntPtr GetModuleHandle(string lpModuleName); + + // Note: Sometimes single VKCode represents multiple chars, thus string. + // E.g. typing "^1" (notice that when pressing 1 the both characters appear, + // because of this behavior, "^" is called dead key) + [DllImport("user32.dll")] +#pragma warning disable CA1838 // Éviter les paramètres 'StringBuilder' pour les P/Invoke + private static extern int ToUnicodeEx(uint wVirtKey, uint wScanCode, byte[] lpKeyState, [Out, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pwszBuff, int cchBuff, uint wFlags, IntPtr dwhkl); +#pragma warning restore CA1838 // Éviter les paramètres 'StringBuilder' pour les P/Invoke + + [DllImport("user32.dll")] + private static extern bool GetKeyboardState(byte[] lpKeyState); + + [DllImport("user32.dll")] + private static extern uint MapVirtualKeyEx(uint uCode, uint uMapType, IntPtr dwhkl); + + [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + private static extern IntPtr GetKeyboardLayout(uint dwLayout); + + [DllImport("User32.dll")] + private static extern IntPtr GetForegroundWindow(); + + [DllImport("User32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [DllImport("user32.dll")] + private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach); + + [DllImport("kernel32.dll")] + private static extern uint GetCurrentThreadId(); + + private static uint lastVKCode; + private static uint lastScanCode; + private static byte[] lastKeyState = new byte[255]; + + /// + /// Convert VKCode to Unicode. + /// isKeyDown is required for because of keyboard state inconsistencies! + /// + /// VKCode + /// Is the key down event? + /// String representing single unicode character. + public static string VKCodeToString(uint vKCode, bool isKeyDown) + { + // ToUnicodeEx needs StringBuilder, it populates that during execution. + System.Text.StringBuilder sbString = new System.Text.StringBuilder(5); + + byte[] bKeyState = new byte[255]; + bool bKeyStateStatus; + + // Gets the current windows window handle, threadID, processID + IntPtr currentHWnd = GetForegroundWindow(); + uint currentProcessID; + uint currentWindowThreadID = GetWindowThreadProcessId(currentHWnd, out currentProcessID); + + // This programs Thread ID + uint thisProgramThreadId = GetCurrentThreadId(); + + // Attach to active thread so we can get that keyboard state + if (AttachThreadInput(thisProgramThreadId, currentWindowThreadID, true)) + { + // Current state of the modifiers in keyboard + bKeyStateStatus = GetKeyboardState(bKeyState); + + // Detach + AttachThreadInput(thisProgramThreadId, currentWindowThreadID, false); + } + else + { + // Could not attach, perhaps it is this process? + bKeyStateStatus = GetKeyboardState(bKeyState); + } + + // On failure we return empty string. + if (!bKeyStateStatus) + { + return string.Empty; + } + + // Gets the layout of keyboard + IntPtr hkl = GetKeyboardLayout(currentWindowThreadID); + + // Maps the virtual keycode + uint lScanCode = MapVirtualKeyEx(vKCode, 0, hkl); + + // Keyboard state goes inconsistent if this is not in place. In other words, we need to call above commands in UP events also. + if (!isKeyDown) + { + return string.Empty; + } + + // Converts the VKCode to unicode + const uint wFlags = 1 << 2; // If bit 2 is set, keyboard state is not changed (Windows 10, version 1607 and newer) + int relevantKeyCountInBuffer = ToUnicodeEx(vKCode, lScanCode, bKeyState, sbString, sbString.Capacity, wFlags, hkl); + + string ret = string.Empty; + + switch (relevantKeyCountInBuffer) + { + // dead key + case -1: + break; + + case 0: + break; + + // Single character in buffer + case 1: + ret = sbString.Length == 0 ? string.Empty : sbString[0].ToString(); + break; + + // Two or more (only two of them is relevant) + case 2: + default: + ret = sbString.ToString().Substring(0, 2); + break; + } + + // Save these + lastScanCode = lScanCode; + lastVKCode = vKCode; + lastKeyState = (byte[])bKeyState.Clone(); + + return ret; + } +} diff --git a/src/modules/poweraccent/PowerAccent.Core/Tools/Logger.cs b/src/modules/poweraccent/PowerAccent.Core/Tools/Logger.cs new file mode 100644 index 0000000000..760a046237 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/Tools/Logger.cs @@ -0,0 +1,79 @@ +// 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.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Abstractions; +using interop; + +namespace PowerAccent.Core.Tools +{ + public static class Logger + { + private static readonly IFileSystem _fileSystem = new FileSystem(); + private static readonly string ApplicationLogPath = Path.Combine(Constants.AppDataPath(), "PowerAccent\\Logs"); + + static Logger() + { + if (!_fileSystem.Directory.Exists(ApplicationLogPath)) + { + _fileSystem.Directory.CreateDirectory(ApplicationLogPath); + } + + // Using InvariantCulture since this is used for a log file name + var logFilePath = _fileSystem.Path.Combine(ApplicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".txt"); + + Trace.Listeners.Add(new TextWriterTraceListener(logFilePath)); + + Trace.AutoFlush = true; + } + + public static void LogError(string message) + { + Log(message, "ERROR"); + } + + public static void LogError(string message, Exception ex) + { + Log( + message + Environment.NewLine + + ex?.Message + Environment.NewLine + + "Inner exception: " + Environment.NewLine + + ex?.InnerException?.Message + Environment.NewLine + + "Stack trace: " + Environment.NewLine + + ex?.StackTrace, + "ERROR"); + } + + public static void LogWarning(string message) + { + Log(message, "WARNING"); + } + + public static void LogInfo(string message) + { + Log(message, "INFO"); + } + + private static void Log(string message, string type) + { + Trace.WriteLine(type + ": " + DateTime.Now.TimeOfDay); + Trace.Indent(); + Trace.WriteLine(GetCallerInfo()); + Trace.WriteLine(message); + Trace.Unindent(); + } + + private static string GetCallerInfo() + { + StackTrace stackTrace = new StackTrace(); + + var methodName = stackTrace.GetFrame(3)?.GetMethod(); + var className = methodName?.DeclaringType?.Name; + return "[Method]: " + methodName?.Name + " [Class]: " + className; + } + } +} diff --git a/src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs b/src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs new file mode 100644 index 0000000000..535057ea48 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.Core/Tools/WindowsFunctions.cs @@ -0,0 +1,78 @@ +// 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.Runtime.InteropServices; +using Vanara.PInvoke; + +namespace PowerAccent.Core.Tools; + +internal static class WindowsFunctions +{ + public static void Insert(char c, bool back = false) + { + unsafe + { + if (back) + { + // Split in 2 different SendInput (Powershell doesn't take back issue) + var inputsBack = new User32.INPUT[] + { + new User32.INPUT { type = User32.INPUTTYPE.INPUT_KEYBOARD, ki = new User32.KEYBDINPUT { wVk = (ushort)User32.VK.VK_BACK } }, + new User32.INPUT { type = User32.INPUTTYPE.INPUT_KEYBOARD, ki = new User32.KEYBDINPUT { wVk = (ushort)User32.VK.VK_BACK, dwFlags = User32.KEYEVENTF.KEYEVENTF_KEYUP } }, + }; + + var temp1 = User32.SendInput((uint)inputsBack.Length, inputsBack, sizeof(User32.INPUT)); + System.Threading.Thread.Sleep(1); // Some apps, like Terminal, need a little wait to process the sent backspace or they'll ignore it. + } + + // Letter + var inputsInsert = new User32.INPUT[1] + { + new User32.INPUT { type = User32.INPUTTYPE.INPUT_KEYBOARD, ki = new User32.KEYBDINPUT { wVk = 0, dwFlags = User32.KEYEVENTF.KEYEVENTF_UNICODE, wScan = c } }, + }; + var temp2 = User32.SendInput((uint)inputsInsert.Length, inputsInsert, sizeof(User32.INPUT)); + } + } + + public static Point GetCaretPosition() + { + User32.GUITHREADINFO guiInfo = new (); + guiInfo.cbSize = (uint)Marshal.SizeOf(guiInfo); + User32.GetGUIThreadInfo(0, ref guiInfo); + System.Drawing.Point caretPosition = new System.Drawing.Point(guiInfo.rcCaret.left, guiInfo.rcCaret.top); + User32.ClientToScreen(guiInfo.hwndCaret, ref caretPosition); + + if (caretPosition.X == 0) + { + System.Drawing.Point testPoint; + User32.GetCaretPos(out testPoint); + return testPoint; + } + + return caretPosition; + } + + public static (Point Location, Size Size, double Dpi) GetActiveDisplay() + { + User32.GUITHREADINFO guiInfo = new (); + guiInfo.cbSize = (uint)Marshal.SizeOf(guiInfo); + User32.GetGUIThreadInfo(0, ref guiInfo); + var res = User32.MonitorFromWindow(guiInfo.hwndActive, User32.MonitorFlags.MONITOR_DEFAULTTONEAREST); + + User32.MONITORINFO monitorInfo = new (); + monitorInfo.cbSize = (uint)Marshal.SizeOf(monitorInfo); + User32.GetMonitorInfo(res, ref monitorInfo); + + double dpi = User32.GetDpiForWindow(guiInfo.hwndActive) / 96d; + + return (monitorInfo.rcWork.Location, monitorInfo.rcWork.Size, dpi); + } + + public static bool IsCapitalState() + { + var capital = User32.GetKeyState((int)User32.VK.VK_CAPITAL); + var shift = User32.GetKeyState((int)User32.VK.VK_SHIFT); + return capital != 0 || shift < 0; + } +} diff --git a/src/modules/poweraccent/PowerAccent.UI/App.xaml b/src/modules/poweraccent/PowerAccent.UI/App.xaml new file mode 100644 index 0000000000..06cd066929 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.UI/App.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/src/modules/poweraccent/PowerAccent.UI/App.xaml.cs b/src/modules/poweraccent/PowerAccent.UI/App.xaml.cs new file mode 100644 index 0000000000..208a15c6db --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.UI/App.xaml.cs @@ -0,0 +1,33 @@ +// 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.Threading; +using System.Windows; + +namespace PowerAccent.UI +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + private static Mutex _mutex; + + protected override void OnStartup(StartupEventArgs e) + { + const string appName = "PowerAccent"; + bool createdNew; + + _mutex = new Mutex(true, appName, out createdNew); + + if (!createdNew) + { + // app is already running! Exiting the application + Application.Current.Shutdown(); + } + + base.OnStartup(e); + } + } +} diff --git a/src/modules/poweraccent/PowerAccent.UI/AssemblyInfo.cs b/src/modules/poweraccent/PowerAccent.UI/AssemblyInfo.cs new file mode 100644 index 0000000000..bcac370d7d --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.UI/AssemblyInfo.cs @@ -0,0 +1,10 @@ +// 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.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, // where theme specific resource dictionaries are located (used if a resource is not found in the page, or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly) // where the generic resource dictionary is locate (used if a resource is not found in the page, app, or any theme specific resource dictionaries) +] diff --git a/src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj b/src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj new file mode 100644 index 0000000000..9404816655 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.UI/PowerAccent.UI.csproj @@ -0,0 +1,35 @@ + + + + WinExe + net6.0-windows + win-x64;win-arm64 + disable + true + True + a-icon.ico + PowerAccent + True + + + + + + + + + PreserveNewest + + + + + + + + + + + + + + diff --git a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml new file mode 100644 index 0000000000..8e6b646c85 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs new file mode 100644 index 0000000000..19d4c8e6d2 --- /dev/null +++ b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs @@ -0,0 +1,71 @@ +// 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.Windows; +using Point = PowerAccent.Core.Point; +using Size = PowerAccent.Core.Size; + +namespace PowerAccent.UI; + +public partial class Selector : Window, IDisposable +{ + private Core.PowerAccent _powerAccent = new Core.PowerAccent(); + + public Selector() + { + InitializeComponent(); + Application.Current.MainWindow.ShowActivated = false; + Application.Current.MainWindow.Topmost = true; + } + + protected override void OnSourceInitialized(EventArgs e) + { + base.OnSourceInitialized(e); + _powerAccent.OnChangeDisplay += PowerAccent_OnChangeDisplay; + _powerAccent.OnSelectCharacter += PowerAccent_OnSelectionCharacter; + this.Visibility = Visibility.Hidden; + } + + private void PowerAccent_OnSelectionCharacter(int index, char character) + { + characters.SelectedIndex = index; + } + + private void PowerAccent_OnChangeDisplay(bool isActive, char[] chars) + { + this.Visibility = isActive ? Visibility.Visible : Visibility.Collapsed; + if (isActive) + { + CenterWindow(); + characters.ItemsSource = chars; + Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new PowerAccent.Core.Telemetry.PowerAccentShowAccentMenuEvent()); + } + } + + private void MenuExit_Click(object sender, RoutedEventArgs e) + { + Application.Current.Shutdown(); + } + + private void CenterWindow() + { + UpdateLayout(); + Size window = new Size(((System.Windows.Controls.Panel)Application.Current.MainWindow.Content).ActualWidth, ((System.Windows.Controls.Panel)Application.Current.MainWindow.Content).ActualHeight); + Point position = _powerAccent.GetDisplayCoordinates(window); + this.Left = position.X; + this.Top = position.Y; + } + + protected override void OnClosed(EventArgs e) + { + _powerAccent.Dispose(); + base.OnClosed(e); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/src/modules/poweraccent/PowerAccent.UI/a-icon.ico b/src/modules/poweraccent/PowerAccent.UI/a-icon.ico new file mode 100644 index 0000000000..1db0b4191c Binary files /dev/null and b/src/modules/poweraccent/PowerAccent.UI/a-icon.ico differ diff --git a/src/modules/poweraccent/PowerAccent.UI/win11desktop.jpg b/src/modules/poweraccent/PowerAccent.UI/win11desktop.jpg new file mode 100644 index 0000000000..f331b98a9d Binary files /dev/null and b/src/modules/poweraccent/PowerAccent.UI/win11desktop.jpg differ diff --git a/src/modules/poweraccent/PowerAccent/PowerAccent.csproj b/src/modules/poweraccent/PowerAccent/PowerAccent.csproj new file mode 100644 index 0000000000..b18864839a --- /dev/null +++ b/src/modules/poweraccent/PowerAccent/PowerAccent.csproj @@ -0,0 +1,22 @@ + + + + WinExe + net6.0-windows + win-x64;win-arm64 + true + disable + PowerAccent.Program + $(SolutionDir)$(Platform)\$(Configuration)\modules\PowerAccent + false + false + PowerToys.PowerAccent + + + + + + + + + diff --git a/src/modules/poweraccent/PowerAccent/Program.cs b/src/modules/poweraccent/PowerAccent/Program.cs new file mode 100644 index 0000000000..8fb46d735b --- /dev/null +++ b/src/modules/poweraccent/PowerAccent/Program.cs @@ -0,0 +1,96 @@ +// 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 warning disable SA1310 // FieldNamesMustNotContainUnderscore + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using interop; +using ManagedCommon; +using PowerAccent.Core.Tools; +using PowerAccent.UI; + +namespace PowerAccent; + +internal static class Program +{ + private const string PROGRAM_NAME = "PowerAccent"; + private const string PROGRAM_APP_NAME = "PowerToys.PowerAccent"; + private static App _application; + private static int _powerToysRunnerPid; + private static CancellationTokenSource _tokenSource = new CancellationTokenSource(); + + [STAThread] + public static void Main(string[] args) + { + _ = new Mutex(true, PROGRAM_APP_NAME, out bool instantiated); + + if (instantiated) + { + Arguments(args); + + InitEvents(); + + _application = new App(); + _application.InitializeComponent(); + _application.Run(); + } + else + { + Logger.LogWarning("Another running PowerAccent instance was detected. Exiting PowerAccent"); + } + } + + private static void InitEvents() + { + Task.Run( + () => + { + EventWaitHandle eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.PowerAccentExitEvent()); + if (eventHandle.WaitOne()) + { + Terminate(); + } + }, _tokenSource.Token); + } + + private static void Arguments(string[] args) + { + if (args?.Length > 0) + { + try + { + _ = int.TryParse(args[0], out _powerToysRunnerPid); + + Logger.LogInfo($"PowerAccent started from the PowerToys Runner. Runner pid={_powerToysRunnerPid}"); + + RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () => + { + Logger.LogInfo("PowerToys Runner exited. Exiting PowerAccent"); + Terminate(); + }); + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + } + } + else + { + Logger.LogInfo($"PowerAccent started detached from PowerToys Runner."); + _powerToysRunnerPid = -1; + } + } + + private static void Terminate() + { + Application.Current.Dispatcher.BeginInvoke(() => + { + _tokenSource.Cancel(); + Application.Current.Shutdown(); + }); + } +} diff --git a/src/modules/poweraccent/PowerAccent/win11desktop.jpg b/src/modules/poweraccent/PowerAccent/win11desktop.jpg new file mode 100644 index 0000000000..f331b98a9d Binary files /dev/null and b/src/modules/poweraccent/PowerAccent/win11desktop.jpg differ diff --git a/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentConstants.h b/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentConstants.h new file mode 100644 index 0000000000..853207c256 --- /dev/null +++ b/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentConstants.h @@ -0,0 +1,7 @@ +#include + +namespace PowerAccentConstants +{ + // Name of the powertoy module. + inline const std::wstring ModuleKey = L"PowerAccent"; +} \ No newline at end of file diff --git a/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.rc b/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.rc new file mode 100644 index 0000000000..c2c40747ae --- /dev/null +++ b/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.rc @@ -0,0 +1,108 @@ +// Microsoft Visual C++ generated resource script. +// +#include +#include "resource.h" +#include "../../../common/version/version.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +1 VERSIONINFO +FILEVERSION FILE_VERSION +PRODUCTVERSION PRODUCT_VERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG +FILEFLAGS VS_FF_DEBUG +#else +FILEFLAGS 0x0L +#endif +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset + BEGIN + VALUE "CompanyName", COMPANY_NAME + VALUE "FileDescription", FILE_DESCRIPTION + VALUE "FileVersion", FILE_VERSION_STRING + VALUE "InternalName", INTERNAL_NAME + VALUE "LegalCopyright", COPYRIGHT_NOTE + VALUE "OriginalFilename", ORIGINAL_FILENAME + VALUE "ProductName", PRODUCT_NAME + VALUE "ProductVersion", PRODUCT_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset + END +END + + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_POWERACCENT_NAME "PowerAccent" +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.vcxproj b/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.vcxproj new file mode 100644 index 0000000000..8ed1ab881a --- /dev/null +++ b/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.vcxproj @@ -0,0 +1,82 @@ + + + + + 15.0 + {34A354C5-23C7-4343-916C-C52DAF4FC39D} + Win32Proj + PowerAccent + PowerAccentModuleInterface + 10.0.19041.0 + + + + DynamicLibrary + + + + + + + + + + + + $(SolutionDir)$(Platform)\$(Configuration)\modules\PowerAccent\ + + + PowerToys.$(ProjectName) + + + + EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) + + + $(OutDir)$(TargetName)$(TargetExt) + + + + + + + + + + + + Create + pch.h + + + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.vcxproj.filters b/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.vcxproj.filters new file mode 100644 index 0000000000..51ce22cb86 --- /dev/null +++ b/src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.vcxproj.filters @@ -0,0 +1,50 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Generated Files + + + + + {e8ef1c4e-cc50-4ce5-b00d-4e3ac5c1a7db} + + + {fbd9cdd2-e7d5-4417-9b52-25e345ae9562} + + + {c2a23a2b-5846-440f-b29e-eea748dba12d} + + + {77f1702b-da7f-4ff6-90a3-19db515cf963} + + + + + + + + Resource Files + + + \ No newline at end of file diff --git a/src/modules/poweraccent/PowerAccentModuleInterface/dllmain.cpp b/src/modules/poweraccent/PowerAccentModuleInterface/dllmain.cpp new file mode 100644 index 0000000000..53badbaa34 --- /dev/null +++ b/src/modules/poweraccent/PowerAccentModuleInterface/dllmain.cpp @@ -0,0 +1,186 @@ +#include "pch.h" +#include +#include +#include +#include "trace.h" +#include "resource.h" +#include "PowerAccentConstants.h" +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + Trace::RegisterProvider(); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + case DLL_PROCESS_DETACH: + Trace::UnregisterProvider(); + break; + } + return TRUE; +} + +const static wchar_t* MODULE_NAME = L"PowerAccent"; +const static wchar_t* MODULE_DESC = L"A module that keeps your computer PowerAccent on-demand."; + +class PowerAccent : public PowertoyModuleIface +{ + std::wstring app_name; + std::wstring app_key; + +private: + bool m_enabled = false; + HANDLE m_hInvokeEvent; + PROCESS_INFORMATION p_info; + + bool is_process_running() + { + return WaitForSingleObject(p_info.hProcess, 0) == WAIT_TIMEOUT; + } + + void launch_process() + { + Logger::trace(L"Launching PowerToys PowerAccent process"); + unsigned long powertoys_pid = GetCurrentProcessId(); + + std::wstring executable_args = L"" + std::to_wstring(powertoys_pid); + std::wstring application_path = L"modules\\PowerAccent\\PowerToys.PowerAccent.exe"; + std::wstring full_command_path = application_path + L" " + executable_args.data(); + Logger::trace(L"PowerToys PowerAccent launching: " + full_command_path); + + STARTUPINFO info = { sizeof(info) }; + + if (!CreateProcess(application_path.c_str(), full_command_path.data(), NULL, NULL, true, NULL, NULL, NULL, &info, &p_info)) + { + DWORD error = GetLastError(); + std::wstring message = L"PowerToys PowerAccent failed to start with error: "; + message += std::to_wstring(error); + Logger::error(message); + } + } + +public: + PowerAccent() + { + app_name = MODULE_NAME; + app_key = PowerAccentConstants::ModuleKey; + LoggerHelpers::init_logger(app_key, L"ModuleInterface", "PowerAccent"); + Logger::info("Launcher object is constructing"); + }; + + virtual void destroy() override + { + delete this; + } + + virtual const wchar_t* get_name() override + { + return MODULE_NAME; + } + + virtual bool get_config(wchar_t* buffer, int* buffer_size) override + { + HINSTANCE hinstance = reinterpret_cast(&__ImageBase); + + PowerToysSettings::Settings settings(hinstance, get_name()); + settings.set_description(MODULE_DESC); + + return settings.serialize_to_buffer(buffer, buffer_size); + } + + virtual const wchar_t* get_key() override + { + return app_key.c_str(); + } + + virtual void set_config(const wchar_t* config) override + { + try + { + // Parse the input JSON string. + PowerToysSettings::PowerToyValues values = + PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); + + // If you don't need to do any custom processing of the settings, proceed + // to persists the values. + values.save_to_settings_file(); + } + catch (std::exception&) + { + // Improper JSON. + } + } + + virtual void enable() + { + ResetEvent(m_hInvokeEvent); + launch_process(); + m_enabled = true; + Trace::EnablePowerAccent(true); + }; + + virtual void disable() + { + if (m_enabled) + { + Logger::trace(L"Disabling PowerAccent... {}", m_enabled); + ResetEvent(m_hInvokeEvent); + + auto exitEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::POWERACCENT_EXIT_EVENT); + if (!exitEvent) + { + Logger::warn(L"Failed to create exit event for PowerToys PowerAccent. {}", get_last_error_or_default(GetLastError())); + } + else + { + Logger::trace(L"Signaled exit event for PowerToys PowerAccent."); + if (!SetEvent(exitEvent)) + { + Logger::warn(L"Failed to signal exit event for PowerToys PowerAccent. {}", get_last_error_or_default(GetLastError())); + + // For some reason, we couldn't process the signal correctly, so we still + // need to terminate the PowerAccent process. + TerminateProcess(p_info.hProcess, 1); + } + + ResetEvent(exitEvent); + CloseHandle(exitEvent); + CloseHandle(p_info.hProcess); + } + } + + m_enabled = false; + Trace::EnablePowerAccent(false); + } + + virtual bool is_enabled() override + { + return m_enabled; + } + + // Returns whether the PowerToys should be enabled by default + virtual bool is_enabled_by_default() const override + { + return false; + } +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new PowerAccent(); +} \ No newline at end of file diff --git a/src/modules/poweraccent/PowerAccentModuleInterface/packages.config b/src/modules/poweraccent/PowerAccentModuleInterface/packages.config new file mode 100644 index 0000000000..6a0a5a1e09 --- /dev/null +++ b/src/modules/poweraccent/PowerAccentModuleInterface/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/poweraccent/PowerAccentModuleInterface/pch.cpp b/src/modules/poweraccent/PowerAccentModuleInterface/pch.cpp new file mode 100644 index 0000000000..17305716aa --- /dev/null +++ b/src/modules/poweraccent/PowerAccentModuleInterface/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" \ No newline at end of file diff --git a/src/modules/poweraccent/PowerAccentModuleInterface/pch.h b/src/modules/poweraccent/PowerAccentModuleInterface/pch.h new file mode 100644 index 0000000000..eddac0fdc1 --- /dev/null +++ b/src/modules/poweraccent/PowerAccentModuleInterface/pch.h @@ -0,0 +1,7 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/src/modules/poweraccent/PowerAccentModuleInterface/resource.h b/src/modules/poweraccent/PowerAccentModuleInterface/resource.h new file mode 100644 index 0000000000..429b09e043 --- /dev/null +++ b/src/modules/poweraccent/PowerAccentModuleInterface/resource.h @@ -0,0 +1,26 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by PowerAccent.rc +// +#define IDS_POWERACCENT_NAME 101 + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys PowerAccent Module" +#define INTERNAL_NAME "PowerToys.PowerAccentModuleInterface" +#define ORIGINAL_FILENAME "PowerToys.PowerAccentModuleInterface.dll" + +// Non-localizable +////////////////////////////// + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/modules/poweraccent/PowerAccentModuleInterface/trace.cpp b/src/modules/poweraccent/PowerAccentModuleInterface/trace.cpp new file mode 100644 index 0000000000..c5f15cc216 --- /dev/null +++ b/src/modules/poweraccent/PowerAccentModuleInterface/trace.cpp @@ -0,0 +1,29 @@ +#include "pch.h" +#include "trace.h" + +TRACELOGGING_DEFINE_PROVIDER( + g_hProvider, + "Microsoft.PowerToys", + // {38e8889b-9731-53f5-e901-e8a7c1753074} + (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74), + TraceLoggingOptionProjectTelemetry()); + +void Trace::RegisterProvider() +{ + TraceLoggingRegister(g_hProvider); +} + +void Trace::UnregisterProvider() +{ + TraceLoggingUnregister(g_hProvider); +} + +void Trace::EnablePowerAccent(const bool enabled) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "PowerAccent_EnablePowerAccent", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, "Enabled")); +} diff --git a/src/modules/poweraccent/PowerAccentModuleInterface/trace.h b/src/modules/poweraccent/PowerAccentModuleInterface/trace.h new file mode 100644 index 0000000000..42b812f535 --- /dev/null +++ b/src/modules/poweraccent/PowerAccentModuleInterface/trace.h @@ -0,0 +1,11 @@ +#pragma once + +class Trace +{ +public: + static void RegisterProvider(); + static void UnregisterProvider(); + + // Log if the user has PowerAccent enabled or disabled + static void EnablePowerAccent(const bool enabled) noexcept; +}; diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 437380b4c5..3a09a1799c 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -153,8 +153,8 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"modules/MouseUtils/PowerToys.MouseHighlighter.dll", L"modules/AlwaysOnTop/PowerToys.AlwaysOnTopModuleInterface.dll", L"modules/MouseUtils/PowerToys.MousePointerCrosshairs.dll", + L"modules/PowerAccent/PowerToys.PowerAccentModuleInterface.dll", L"modules/PowerOCR/PowerToys.PowerOCRModuleInterface.dll", - }; const auto VCM_PATH = L"modules/VideoConference/PowerToys.VideoConferenceModule.dll"; if (const auto mf = LoadLibraryA("mf.dll")) diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs index 906f68cdda..b3052c47c3 100644 --- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs @@ -239,6 +239,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + private bool powerAccent; + + [JsonPropertyName("PowerAccent")] + public bool PowerAccent + { + get => powerAccent; + set + { + if (powerAccent != value) + { + LogTelemetryEvent(value); + powerAccent = value; + } + } + } + private bool powerOCR = true; [JsonPropertyName("PowerOCR")] diff --git a/src/settings-ui/Settings.UI.Library/Enumerations/PowerAccentActivationKey.cs b/src/settings-ui/Settings.UI.Library/Enumerations/PowerAccentActivationKey.cs new file mode 100644 index 0000000000..85ae31fba5 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Enumerations/PowerAccentActivationKey.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.PowerToys.Settings.UI.Library.Enumerations +{ + public enum PowerAccentActivationKey + { + LeftRightArrow, + Space, + Both, + } +} diff --git a/src/settings-ui/Settings.UI.Library/PowerAccentProperties.cs b/src/settings-ui/Settings.UI.Library/PowerAccentProperties.cs new file mode 100644 index 0000000000..cde414cc71 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/PowerAccentProperties.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Enumerations; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class PowerAccentProperties + { + [JsonPropertyName("activation_key")] + public PowerAccentActivationKey ActivationKey { get; set; } + + [JsonPropertyName("toolbar_position")] + public StringProperty ToolbarPosition { get; set; } + + public PowerAccentProperties() + { + ActivationKey = PowerAccentActivationKey.Both; + ToolbarPosition = "Top center"; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/PowerAccentSettings.cs b/src/settings-ui/Settings.UI.Library/PowerAccentSettings.cs new file mode 100644 index 0000000000..cb37a895e4 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/PowerAccentSettings.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class PowerAccentSettings : BasePTModuleSettings, ISettingsConfig + { + public const string ModuleName = "PowerAccent"; + public const string ModuleVersion = "0.0.1"; + + [JsonPropertyName("properties")] + public PowerAccentProperties Properties { get; set; } + + public PowerAccentSettings() + { + Name = ModuleName; + Version = ModuleVersion; + Properties = new PowerAccentProperties(); + } + + public string GetModuleName() + { + return Name; + } + + public bool UpgradeSettingsConfiguration() + { + return false; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/SndPowerAccentSettings.cs b/src/settings-ui/Settings.UI.Library/SndPowerAccentSettings.cs new file mode 100644 index 0000000000..ca52dcdfc9 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/SndPowerAccentSettings.cs @@ -0,0 +1,29 @@ +// 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; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class SndPowerAccentSettings + { + [JsonPropertyName("PowerAccent")] + public PowerAccentSettings PowerAccentSettings { get; set; } + + public SndPowerAccentSettings() + { + } + + public SndPowerAccentSettings(PowerAccentSettings settings) + { + PowerAccentSettings = settings; + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/ViewModels/PowerAccentViewModel.cs b/src/settings-ui/Settings.UI.Library/ViewModels/PowerAccentViewModel.cs new file mode 100644 index 0000000000..b895c8eb14 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/ViewModels/PowerAccentViewModel.cs @@ -0,0 +1,187 @@ +// 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.Runtime.CompilerServices; +using Microsoft.PowerToys.Settings.UI.Library.Enumerations; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels +{ + public class PowerAccentViewModel : Observable + { + private GeneralSettings GeneralSettingsConfig { get; set; } + + private readonly PowerAccentSettings _powerAccentSettings; + + private readonly ISettingsUtils _settingsUtils; + + private Func SendConfigMSG { get; } + + public PowerAccentViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, Func ipcMSGCallBackFunc) + { + // To obtain the general settings configurations of PowerToys Settings. + if (settingsRepository == null) + { + throw new ArgumentNullException(nameof(settingsRepository)); + } + + _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils)); + GeneralSettingsConfig = settingsRepository.SettingsConfig; + + _isEnabled = GeneralSettingsConfig.Enabled.PowerAccent; + if (_settingsUtils.SettingsExists(PowerAccentSettings.ModuleName)) + { + _powerAccentSettings = _settingsUtils.GetSettingsOrDefault(PowerAccentSettings.ModuleName); + } + else + { + _powerAccentSettings = new PowerAccentSettings(); + } + + switch (_powerAccentSettings.Properties.ToolbarPosition.Value) + { + case "Top center": + _toolbarPositionIndex = 0; + break; + case "Bottom center": + _toolbarPositionIndex = 1; + break; + case "Left": + _toolbarPositionIndex = 2; + break; + case "Right": + _toolbarPositionIndex = 3; + break; + case "Top right corner": + _toolbarPositionIndex = 4; + break; + case "Top left corner": + _toolbarPositionIndex = 5; + break; + case "Bottom right corner": + _toolbarPositionIndex = 6; + break; + case "Bottom left corner": + _toolbarPositionIndex = 7; + break; + case "Center": + _toolbarPositionIndex = 8; + break; + } + + // set the callback functions value to hangle outgoing IPC message. + SendConfigMSG = ipcMSGCallBackFunc; + } + + public bool IsEnabled + { + get => _isEnabled; + set + { + if (_isEnabled != value) + { + _isEnabled = value; + + GeneralSettingsConfig.Enabled.PowerAccent = value; + OnPropertyChanged(nameof(IsEnabled)); + OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); + SendConfigMSG(outgoing.ToString()); + } + } + } + + public int ActivationKey + { + get + { + return (int)_powerAccentSettings.Properties.ActivationKey; + } + + set + { + if (value != (int)_powerAccentSettings.Properties.ActivationKey) + { + _powerAccentSettings.Properties.ActivationKey = (PowerAccentActivationKey)value; + OnPropertyChanged(nameof(ActivationKey)); + RaisePropertyChanged(); + } + } + } + + private int _toolbarPositionIndex; + + public int ToolbarPositionIndex + { + get + { + return _toolbarPositionIndex; + } + + set + { + if (_toolbarPositionIndex != value) + { + _toolbarPositionIndex = value; + switch (_toolbarPositionIndex) + { + case 0: + _powerAccentSettings.Properties.ToolbarPosition.Value = "Top center"; + break; + + case 1: + _powerAccentSettings.Properties.ToolbarPosition.Value = "Bottom center"; + break; + + case 2: + _powerAccentSettings.Properties.ToolbarPosition.Value = "Left"; + break; + + case 3: + _powerAccentSettings.Properties.ToolbarPosition.Value = "Right"; + break; + + case 4: + _powerAccentSettings.Properties.ToolbarPosition.Value = "Top right corner"; + break; + + case 5: + _powerAccentSettings.Properties.ToolbarPosition.Value = "Top left corner"; + break; + + case 6: + _powerAccentSettings.Properties.ToolbarPosition.Value = "Bottom right corner"; + break; + + case 7: + _powerAccentSettings.Properties.ToolbarPosition.Value = "Bottom left corner"; + break; + + case 8: + _powerAccentSettings.Properties.ToolbarPosition.Value = "Center"; + break; + } + + RaisePropertyChanged(nameof(ToolbarPositionIndex)); + } + } + } + + private void RaisePropertyChanged([CallerMemberName] string propertyName = null) + { + // Notify UI of property change + OnPropertyChanged(propertyName); + + if (SendConfigMSG != null) + { + SndPowerAccentSettings snd = new SndPowerAccentSettings(_powerAccentSettings); + SndModuleSettings ipcMessage = new SndModuleSettings(snd); + SendConfigMSG(ipcMessage.ToJsonString()); + } + } + + private bool _isEnabled; + } +} diff --git a/src/settings-ui/Settings.UI/App.xaml.cs b/src/settings-ui/Settings.UI/App.xaml.cs index ea99e47220..a424ffeca6 100644 --- a/src/settings-ui/Settings.UI/App.xaml.cs +++ b/src/settings-ui/Settings.UI/App.xaml.cs @@ -122,6 +122,7 @@ namespace Microsoft.PowerToys.Settings.UI case "ImageResizer": StartupPage = typeof(Views.ImageResizerPage); break; case "KBM": StartupPage = typeof(Views.KeyboardManagerPage); break; case "MouseUtils": StartupPage = typeof(Views.MouseUtilsPage); break; + case "PowerAccent": StartupPage = typeof(Views.PowerAccentPage); break; case "PowerOCR": StartupPage = typeof(Views.PowerOcrPage); break; case "PowerRename": StartupPage = typeof(Views.PowerRenamePage); break; case "FileExplorer": StartupPage = typeof(Views.PowerPreviewPage); break; diff --git a/src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsPowerAccent.png b/src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsPowerAccent.png new file mode 100644 index 0000000000..e247deac88 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsPowerAccent.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Modules/OOBE/PowerAccent.gif b/src/settings-ui/Settings.UI/Assets/Modules/OOBE/PowerAccent.gif new file mode 100644 index 0000000000..758a78037a Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Modules/OOBE/PowerAccent.gif differ diff --git a/src/settings-ui/Settings.UI/Assets/Modules/PowerAccent.png b/src/settings-ui/Settings.UI/Assets/Modules/PowerAccent.png new file mode 100644 index 0000000000..1adb76abcd Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Modules/PowerAccent.png differ diff --git a/src/settings-ui/Settings.UI/Controls/PowerAccentShortcutControl.xaml b/src/settings-ui/Settings.UI/Controls/PowerAccentShortcutControl.xaml new file mode 100644 index 0000000000..82fe8d310d --- /dev/null +++ b/src/settings-ui/Settings.UI/Controls/PowerAccentShortcutControl.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Controls/PowerAccentShortcutControl.xaml.cs b/src/settings-ui/Settings.UI/Controls/PowerAccentShortcutControl.xaml.cs new file mode 100644 index 0000000000..e28282e439 --- /dev/null +++ b/src/settings-ui/Settings.UI/Controls/PowerAccentShortcutControl.xaml.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.PowerToys.Settings.UI.Controls +{ + public sealed partial class PowerAccentShortcutControl : UserControl + { + public string Text + { + get { return (string)GetValue(TextProperty); } + set { SetValue(TextProperty, value); } + } + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(PowerAccentShortcutControl), new PropertyMetadata(default(string))); + +#pragma warning disable CA2227 // Collection properties should be read only + public List Keys +#pragma warning restore CA2227 // Collection properties should be read only + { + get { return (List)GetValue(KeysProperty); } + set { SetValue(KeysProperty, value); } + } + + public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(PowerAccentShortcutControl), new PropertyMetadata(default(string))); + + public PowerAccentShortcutControl() + { + this.InitializeComponent(); + } + } +} diff --git a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs index f7b2f42291..cc566a88d1 100644 --- a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs +++ b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs @@ -15,6 +15,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums ImageResizer, KBM, MouseUtils, + PowerAccent, PowerOCR, PowerRename, Run, diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobePowerAccent.xaml b/src/settings-ui/Settings.UI/OOBE/Views/OobePowerAccent.xaml new file mode 100644 index 0000000000..fa3a44e6ad --- /dev/null +++ b/src/settings-ui/Settings.UI/OOBE/Views/OobePowerAccent.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + +