From 213c946929721efb23e098a9ba6f622f5a80ef82 Mon Sep 17 00:00:00 2001 From: Michael Clayton Date: Fri, 24 Feb 2023 13:30:30 +0000 Subject: [PATCH] [New Utility]Mouse Jump(#23566) * #23216 - initial MouseJump commit * #23216 - Mouse Jump - fix spelling, removing Interop folder * #23216 - Mouse Jump - removed orphaned project guids from PowerToys.sln * #23216 - Mouse Jump - removed orphaned project guids from PowerToys.sln * #23216 - Mouse Jump - switch MS Logger to NLog for nuget package allow-listing * #23216 added MouseJumpUI.UnitTests.dll to "MS Tests" step in build-powertoys-steps.yml * [MouseJump] fixed screenshot coords (x & y were transposed) (#23216) * [MouseJump] close form rather than hide on deactivate (#23216) * [MouseJump] added UI dll for signing (#23216) * [MouseJump] close form rather than hide on deactivate (#23216) * [MouseJump] removed redundant line * [MouseJump] configure dpi awareness, add NLog.config (microsoft#23216) * [MouseJump] fix spellchecker errors (microsoft#23216) * [MouseJump] fixing comment style warning (microsoft#23216) * [MouseJump] simplified dpi config (microsoft#23216) * [MouseJump] fixed edge case issue with moving cursor (microsoft#23216) * [MouseJump] fixed typo (microsoft#23216) * [MouseJump] added attribution (microsoft#23216) * [Mouse Jump] fix attribution link and spelling (microsoft#23216) * Add MouseJump to installer * Fix centralized version control * Add Quick Access enable/disable entry * Fix analyzer error in GPO * Fix botched merge * Disabled by default and remove boilerplate code * Add GPO definitions * Add GPO implications when starting standalone * Update hotkey when it's changed in Settings * Use standard Logger * Add OOBE strings for Mouse Jump * Add telemetry * Update installer * Add signing * Add to bug report tool * Address PR feedback --- .github/actions/spell-check/expect.txt | 2 + .pipelines/ESRPSigning_core.json | 3 + .../ci/templates/build-powertoys-steps.yml | 1 + PowerToys.sln | 45 ++ installer/PowerToysSetup/MouseUtils.wxs | 16 + installer/PowerToysSetup/Product.wxs | 1 + installer/PowerToysSetup/Settings.wxs | 2 +- .../CustomAction.cpp | 15 +- src/common/GPOWrapper/GPOWrapper.cpp | 4 + src/common/GPOWrapper/GPOWrapper.h | 1 + src/common/GPOWrapper/GPOWrapper.idl | 1 + src/common/logger/logger_settings.h | 1 + src/common/utils/gpo.h | 6 + src/gpo/assets/PowerToys.admx | 10 + src/gpo/assets/en-US/PowerToys.adml | 1 + src/modules/MouseUtils/MouseJump/MouseJump.rc | 40 ++ .../MouseUtils/MouseJump/MouseJump.vcxproj | 129 +++++ src/modules/MouseUtils/MouseJump/dllmain.cpp | 302 ++++++++++ .../MouseUtils/MouseJump/packages.config | 4 + src/modules/MouseUtils/MouseJump/pch.cpp | 1 + src/modules/MouseUtils/MouseJump/pch.h | 10 + src/modules/MouseUtils/MouseJump/resource.h | 13 + src/modules/MouseUtils/MouseJump/trace.cpp | 38 ++ src/modules/MouseUtils/MouseJump/trace.h | 12 + .../Helpers/LayoutHelperTests.cs | 514 ++++++++++++++++++ .../MouseJumpUI.UnitTests.csproj | 29 + .../MouseJumpUI/Helpers/LayoutHelper.cs | 191 +++++++ .../MouseUtils/MouseJumpUI/Helpers/Logger.cs | 77 +++ .../MouseJumpUI/MainForm.Designer.cs | 95 ++++ .../MouseUtils/MouseJumpUI/MainForm.cs | 177 ++++++ .../MouseUtils/MouseJumpUI/MainForm.resx | 317 +++++++++++ .../MouseUtils/MouseJumpUI/MouseJumpUI.csproj | 66 +++ src/modules/MouseUtils/MouseJumpUI/Program.cs | 38 ++ .../MouseJumpUI/Properties/AssemblyInfo.cs | 7 + .../Telemetry/MouseJumpShowEvent.cs | 16 + .../Telemetry/MouseJumpTeleportCursorEvent.cs | 16 + src/runner/main.cpp | 1 + .../Settings.UI.Library/EnabledModules.cs | 16 + .../MouseJumpProperties.cs | 19 + .../Settings.UI.Library/MouseJumpSettings.cs | 35 ++ .../SndMouseJumpSettings.cs | 29 + .../FluentIcons/FluentIconsMouseJump.png | Bin 0 -> 1510 bytes .../Settings.UI/MainWindow.xaml.cs | 3 + .../OOBE/Views/OobeMouseUtils.xaml | 7 + .../Settings.UI/Strings/en-us/Resources.resw | 26 + .../ViewModels/Flyout/AllAppsViewModel.cs | 6 + .../ViewModels/MouseUtilsViewModel.cs | 86 ++- .../Settings.UI/Views/MouseUtilsPage.xaml | 28 + .../Settings.UI/Views/MouseUtilsPage.xaml.cs | 1 + .../BugReportTool/ProcessesList.cpp | 1 + .../BugReportTool/ReportGPOValues.cpp | 1 + 51 files changed, 2455 insertions(+), 5 deletions(-) create mode 100644 src/modules/MouseUtils/MouseJump/MouseJump.rc create mode 100644 src/modules/MouseUtils/MouseJump/MouseJump.vcxproj create mode 100644 src/modules/MouseUtils/MouseJump/dllmain.cpp create mode 100644 src/modules/MouseUtils/MouseJump/packages.config create mode 100644 src/modules/MouseUtils/MouseJump/pch.cpp create mode 100644 src/modules/MouseUtils/MouseJump/pch.h create mode 100644 src/modules/MouseUtils/MouseJump/resource.h create mode 100644 src/modules/MouseUtils/MouseJump/trace.cpp create mode 100644 src/modules/MouseUtils/MouseJump/trace.h create mode 100644 src/modules/MouseUtils/MouseJumpUI.UnitTests/Helpers/LayoutHelperTests.cs create mode 100644 src/modules/MouseUtils/MouseJumpUI.UnitTests/MouseJumpUI.UnitTests.csproj create mode 100644 src/modules/MouseUtils/MouseJumpUI/Helpers/LayoutHelper.cs create mode 100644 src/modules/MouseUtils/MouseJumpUI/Helpers/Logger.cs create mode 100644 src/modules/MouseUtils/MouseJumpUI/MainForm.Designer.cs create mode 100644 src/modules/MouseUtils/MouseJumpUI/MainForm.cs create mode 100644 src/modules/MouseUtils/MouseJumpUI/MainForm.resx create mode 100644 src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj create mode 100644 src/modules/MouseUtils/MouseJumpUI/Program.cs create mode 100644 src/modules/MouseUtils/MouseJumpUI/Properties/AssemblyInfo.cs create mode 100644 src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpShowEvent.cs create mode 100644 src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpTeleportCursorEvent.cs create mode 100644 src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs create mode 100644 src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs create mode 100644 src/settings-ui/Settings.UI.Library/SndMouseJumpSettings.cs create mode 100644 src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsMouseJump.png diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index a75513123279..cc620ed98f6a 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -216,6 +216,7 @@ cim CImage cla clangformat +clayton CLASSDC CLASSNOTAVAILABLE clickable @@ -536,6 +537,7 @@ EXTENDEDKEY EXTENDEDVERBS eyetracker fabricbot +fancymouse fancyzones FANCYZONESDRAWLAYOUTTEST FANCYZONESEDITOR diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index 96efd6cd4a12..d7173d5fb787 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -132,7 +132,10 @@ "modules\\MouseUtils\\PowerToys.FindMyMouse.dll", "modules\\MouseUtils\\PowerToys.MouseHighlighter.dll", + "modules\\MouseUtils\\PowerToys.MouseJump.dll", "modules\\MouseUtils\\PowerToys.MousePointerCrosshairs.dll", + "modules\\MouseUtils\\MouseJumpUI\\PowerToys.MouseJumpUI.dll", + "modules\\MouseUtils\\MouseJumpUI\\PowerToys.MouseJumpUI.exe", "modules\\PowerAccent\\PowerAccent.Core.dll", "modules\\PowerAccent\\PowerToys.PowerAccent.dll", diff --git a/.pipelines/ci/templates/build-powertoys-steps.yml b/.pipelines/ci/templates/build-powertoys-steps.yml index 703d520f1087..94675840fea5 100644 --- a/.pipelines/ci/templates/build-powertoys-steps.yml +++ b/.pipelines/ci/templates/build-powertoys-steps.yml @@ -208,6 +208,7 @@ steps: **\UnitTests-SvgThumbnailProvider.dll **\UnitTests-SvgPreviewHandler.dll **\PowerToys.Hosts.Tests.dll + **\MouseJumpUI.UnitTests.dll !**\obj\** !**\ref\** diff --git a/PowerToys.sln b/PowerToys.sln index 725839892c6d..783b8ad60568 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -487,6 +487,12 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StlThumbnailProviderCpp", " EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SvgThumbnailProviderCpp", "src\modules\previewpane\SvgThumbnailProviderCpp\SvgThumbnailProviderCpp.vcxproj", "{2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MouseJump", "src\modules\MouseUtils\MouseJump\MouseJump.vcxproj", "{8A08D663-4995-40E3-B42C-3F910625F284}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MouseJumpUI", "src\modules\MouseUtils\MouseJumpUI\MouseJumpUI.csproj", "{D962A009-834F-4EEC-AABB-430DF8F98E39}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MouseJumpUI.UnitTests", "src\modules\MouseUtils\MouseJumpUI.UnitTests\MouseJumpUI.UnitTests.csproj", "{D9C5DE64-6849-4278-91AD-9660AECF2876}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pasteplain", "pasteplain", "{9873BA05-4C41-4819-9283-CF45D795431B}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PastePlainModuleInterface", "src\modules\pasteplain\PastePlainModuleInterface\PastePlainModuleInterface.vcxproj", "{FC373B24-3293-453C-AAF5-CF2909DCEE6A}" @@ -2028,6 +2034,42 @@ Global {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Release|x64.Build.0 = Release|x64 {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Release|x86.ActiveCfg = Release|x64 {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Release|x86.Build.0 = Release|x64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Debug|ARM64.Build.0 = Debug|ARM64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Debug|x64.ActiveCfg = Debug|x64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Debug|x64.Build.0 = Debug|x64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Debug|x86.ActiveCfg = Debug|x64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Debug|x86.Build.0 = Debug|x64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Release|ARM64.ActiveCfg = Release|ARM64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Release|ARM64.Build.0 = Release|ARM64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Release|x64.ActiveCfg = Release|x64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Release|x64.Build.0 = Release|x64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Release|x86.ActiveCfg = Release|x64 + {8A08D663-4995-40E3-B42C-3F910625F284}.Release|x86.Build.0 = Release|x64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Debug|ARM64.Build.0 = Debug|ARM64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Debug|x64.ActiveCfg = Debug|x64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Debug|x64.Build.0 = Debug|x64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Debug|x86.ActiveCfg = Debug|x64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Debug|x86.Build.0 = Debug|x64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Release|ARM64.ActiveCfg = Release|ARM64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Release|ARM64.Build.0 = Release|ARM64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Release|x64.ActiveCfg = Release|x64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Release|x64.Build.0 = Release|x64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Release|x86.ActiveCfg = Release|x64 + {D962A009-834F-4EEC-AABB-430DF8F98E39}.Release|x86.Build.0 = Release|x64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Debug|ARM64.Build.0 = Debug|ARM64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Debug|x64.ActiveCfg = Debug|x64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Debug|x64.Build.0 = Debug|x64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Debug|x86.ActiveCfg = Debug|x64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Debug|x86.Build.0 = Debug|x64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Release|ARM64.ActiveCfg = Release|ARM64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Release|ARM64.Build.0 = Release|ARM64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Release|x64.ActiveCfg = Release|x64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Release|x64.Build.0 = Release|x64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Release|x86.ActiveCfg = Release|x64 + {D9C5DE64-6849-4278-91AD-9660AECF2876}.Release|x86.Build.0 = Release|x64 {FC373B24-3293-453C-AAF5-CF2909DCEE6A}.Debug|ARM64.ActiveCfg = Debug|ARM64 {FC373B24-3293-453C-AAF5-CF2909DCEE6A}.Debug|ARM64.Build.0 = Debug|ARM64 {FC373B24-3293-453C-AAF5-CF2909DCEE6A}.Debug|x64.ActiveCfg = Debug|x64 @@ -2222,6 +2264,9 @@ Global {CA5518ED-0458-4B09-8F53-4122B9888655} = {2F305555-C296-497E-AC20-5FA1B237996A} {D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D} = {2F305555-C296-497E-AC20-5FA1B237996A} {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA} = {2F305555-C296-497E-AC20-5FA1B237996A} + {8A08D663-4995-40E3-B42C-3F910625F284} = {322566EF-20DC-43A6-B9F8-616AF942579A} + {D962A009-834F-4EEC-AABB-430DF8F98E39} = {322566EF-20DC-43A6-B9F8-616AF942579A} + {D9C5DE64-6849-4278-91AD-9660AECF2876} = {322566EF-20DC-43A6-B9F8-616AF942579A} {9873BA05-4C41-4819-9283-CF45D795431B} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} {FC373B24-3293-453C-AAF5-CF2909DCEE6A} = {9873BA05-4C41-4819-9283-CF45D795431B} {9CE59ED5-7087-4353-88EB-788038A73CEC} = {1AFB6476-670D-4E80-A464-657E01DFF482} diff --git a/installer/PowerToysSetup/MouseUtils.wxs b/installer/PowerToysSetup/MouseUtils.wxs index 0d8ed66f1afd..913ac7b5046b 100644 --- a/installer/PowerToysSetup/MouseUtils.wxs +++ b/installer/PowerToysSetup/MouseUtils.wxs @@ -4,6 +4,8 @@ + + @@ -16,12 +18,26 @@ + + + + + + + + + + + + + + diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 8ef113c1b38d..903178678ff6 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -477,6 +477,7 @@ + diff --git a/installer/PowerToysSetup/Settings.wxs b/installer/PowerToysSetup/Settings.wxs index e36bd5e642e7..013ffe958901 100644 --- a/installer/PowerToysSetup/Settings.wxs +++ b/installer/PowerToysSetup/Settings.wxs @@ -7,7 +7,7 @@ - + diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp index c1f9e5a99afb..ed17d9a34c8a 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp @@ -1036,6 +1036,7 @@ const std::wstring PTInteropConsumers[] = L"modules\\FileLocksmith", L"modules\\Hosts", L"modules\\FileExplorerPreview", + L"modules\\MouseUtils\\MouseJumpUI", }; UINT __stdcall CreatePTInteropHardlinksCA(MSIHANDLE hInstall) @@ -1079,7 +1080,8 @@ UINT __stdcall CreateDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) HRESULT hr = S_OK; UINT er = ERROR_SUCCESS; std::wstring installationFolder, dotnetRuntimeFilesSrcDir, colorPickerDir, powerOCRDir, launcherDir, fancyZonesDir, - imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir, fileExplorerAddOnsDir, hostsDir, fileLocksmithDir; + imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir, fileExplorerAddOnsDir, hostsDir, fileLocksmithDir, + mouseJumpDir; hr = WcaInitialize(hInstall, "CreateDotnetRuntimeHardlinksCA"); ExitOnFailure(hr, "Failed to initialize"); @@ -1100,6 +1102,7 @@ UINT __stdcall CreateDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) fileExplorerAddOnsDir = installationFolder + L"modules\\FileExplorerPreview\\"; hostsDir = installationFolder + L"modules\\Hosts\\"; fileLocksmithDir = installationFolder + L"modules\\FileLocksmith\\"; + mouseJumpDir = installationFolder + L"modules\\MouseUtils\\MouseJumpUI\\"; for (auto file : dotnetRuntimeFiles) { @@ -1116,6 +1119,7 @@ UINT __stdcall CreateDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (fileExplorerAddOnsDir + file).c_str(), ec); std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (hostsDir + file).c_str(), ec); std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (fileLocksmithDir + file).c_str(), ec); + std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (mouseJumpDir + file).c_str(), ec); if (ec.value() != S_OK) { @@ -1139,6 +1143,7 @@ UINT __stdcall CreateDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (powerAccentDir + file).c_str(), ec); std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (fileExplorerAddOnsDir + file).c_str(), ec); std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (hostsDir + file).c_str(), ec); + std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (mouseJumpDir + file).c_str(), ec); if (ec.value() != S_OK) { @@ -1233,7 +1238,7 @@ UINT __stdcall DeleteDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) UINT er = ERROR_SUCCESS; std::wstring installationFolder, colorPickerDir, powerOCRDir, launcherDir, fancyZonesDir, imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir, fileExplorerAddOnsDir, - hostsDir, fileLocksmithDir; + hostsDir, fileLocksmithDir, mouseJumpDir; hr = WcaInitialize(hInstall, "DeleteDotnetRuntimeHardlinksCA"); ExitOnFailure(hr, "Failed to initialize"); @@ -1253,6 +1258,7 @@ UINT __stdcall DeleteDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) fileExplorerAddOnsDir = installationFolder + L"modules\\FileExplorerPreview\\"; hostsDir = installationFolder + L"modules\\Hosts\\"; fileLocksmithDir = installationFolder + L"modules\\FileLocksmith\\"; + mouseJumpDir = installationFolder + L"modules\\MouseUtils\\MouseJumpUI\\"; try { @@ -1270,6 +1276,7 @@ UINT __stdcall DeleteDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) DeleteFile((fileExplorerAddOnsDir + file).c_str()); DeleteFile((hostsDir + file).c_str()); DeleteFile((fileLocksmithDir + file).c_str()); + DeleteFile((mouseJumpDir + file).c_str()); } for (auto file : dotnetRuntimeWPFFiles) @@ -1283,6 +1290,7 @@ UINT __stdcall DeleteDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) DeleteFile((powerAccentDir + file).c_str()); DeleteFile((fileExplorerAddOnsDir + file).c_str()); DeleteFile((hostsDir + file).c_str()); + DeleteFile((mouseJumpDir + file).c_str()); } } catch (std::exception e) @@ -1316,13 +1324,14 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) } processes.resize(bytes / sizeof(processes[0])); - std::array processesToTerminate = { + std::array processesToTerminate = { L"PowerToys.PowerLauncher.exe", L"PowerToys.Settings.exe", L"PowerToys.Awake.exe", L"PowerToys.FancyZones.exe", L"PowerToys.FancyZonesEditor.exe", L"PowerToys.FileLocksmithUI.exe", + L"PowerToys.MouseJumpUI.exe", L"PowerToys.ColorPickerUI.exe", L"PowerToys.AlwaysOnTop.exe", L"PowerToys.exe" diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index dba7b29a5d4a..09327b9c3118 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -80,6 +80,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getConfiguredMouseHighlighterEnabledValue()); } + GpoRuleConfigured GPOWrapper::GetConfiguredMouseJumpEnabledValue() + { + return static_cast(powertoys_gpo::getConfiguredMouseJumpEnabledValue()); + } GpoRuleConfigured GPOWrapper::GetConfiguredMousePointerCrosshairsEnabledValue() { return static_cast(powertoys_gpo::getConfiguredMousePointerCrosshairsEnabledValue()); diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index fcb559d03478..0ca33d1224df 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -26,6 +26,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static GpoRuleConfigured GetConfiguredKeyboardManagerEnabledValue(); static GpoRuleConfigured GetConfiguredFindMyMouseEnabledValue(); static GpoRuleConfigured GetConfiguredMouseHighlighterEnabledValue(); + static GpoRuleConfigured GetConfiguredMouseJumpEnabledValue(); static GpoRuleConfigured GetConfiguredMousePointerCrosshairsEnabledValue(); static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue(); static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue(); diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index 51189e48dae4..2b9f631cc7e1 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -30,6 +30,7 @@ namespace PowerToys static GpoRuleConfigured GetConfiguredKeyboardManagerEnabledValue(); static GpoRuleConfigured GetConfiguredFindMyMouseEnabledValue(); static GpoRuleConfigured GetConfiguredMouseHighlighterEnabledValue(); + static GpoRuleConfigured GetConfiguredMouseJumpEnabledValue(); static GpoRuleConfigured GetConfiguredMousePointerCrosshairsEnabledValue(); static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue(); static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue(); diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index b42421231454..bfe35315d833 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -47,6 +47,7 @@ struct LogSettings inline const static std::wstring keyboardManagerLogPath = L"Logs\\keyboard-manager-log.txt"; inline const static std::string findMyMouseLoggerName = "find-my-mouse"; inline const static std::string mouseHighlighterLoggerName = "mouse-highlighter"; + inline const static std::string mouseJumpLoggerName = "mouse-jump"; inline const static std::string mousePointerCrosshairsLoggerName = "mouse-pointer-crosshairs"; inline const static std::string imageResizerLoggerName = "imageresizer"; inline const static std::string powerRenameLoggerName = "powerrename"; diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h index 1e1bcc2140fc..3414129b814d 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -38,6 +38,7 @@ namespace powertoys_gpo { const std::wstring POLICY_CONFIGURE_ENABLED_KEYBOARD_MANAGER = L"ConfigureEnabledUtilityKeyboardManager"; const std::wstring POLICY_CONFIGURE_ENABLED_FIND_MY_MOUSE = L"ConfigureEnabledUtilityFindMyMouse"; const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_HIGHLIGHTER = L"ConfigureEnabledUtilityMouseHighlighter"; + const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_JUMP = L"ConfigureEnabledUtilityMouseJump"; const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_POINTER_CROSSHAIRS = L"ConfigureEnabledUtilityMousePointerCrosshairs"; const std::wstring POLICY_CONFIGURE_ENABLED_POWER_RENAME = L"ConfigureEnabledUtilityPowerRename"; const std::wstring POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER = L"ConfigureEnabledUtilityPowerLauncher"; @@ -197,6 +198,11 @@ namespace powertoys_gpo { return getConfiguredValue(POLICY_CONFIGURE_ENABLED_MOUSE_HIGHLIGHTER); } + inline gpo_rule_configured_t getConfiguredMouseJumpEnabledValue() + { + return getConfiguredValue(POLICY_CONFIGURE_ENABLED_MOUSE_JUMP); + } + inline gpo_rule_configured_t getConfiguredMousePointerCrosshairsEnabledValue() { return getConfiguredValue(POLICY_CONFIGURE_ENABLED_MOUSE_POINTER_CROSSHAIRS); diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx index 4a7fbadafd0d..9ccf25b0a258 100644 --- a/src/gpo/assets/PowerToys.admx +++ b/src/gpo/assets/PowerToys.admx @@ -217,6 +217,16 @@ + + + + + + + + + + diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml index 9dadbafef22f..25ea233de657 100644 --- a/src/gpo/assets/en-US/PowerToys.adml +++ b/src/gpo/assets/en-US/PowerToys.adml @@ -54,6 +54,7 @@ If this setting is disabled, experimentation is not allowed. Keyboard Manager: Configure enabled state Find My Mouse: Configure enabled state Mouse Highlighter: Configure enabled state + Mouse Jump: Configure enabled state Mouse Pointer Crosshairs: Configure enabled state Paste as Plain Text: Configure enabled state Power Rename: Configure enabled state diff --git a/src/modules/MouseUtils/MouseJump/MouseJump.rc b/src/modules/MouseUtils/MouseJump/MouseJump.rc new file mode 100644 index 000000000000..f8d0a54cdab6 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump/MouseJump.rc @@ -0,0 +1,40 @@ +#include +#include "resource.h" +#include "../../../common/version/version.h" + +#define APSTUDIO_READONLY_SYMBOLS +#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 \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj b/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj new file mode 100644 index 000000000000..16b9692ffdf9 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj @@ -0,0 +1,129 @@ + + + + + 15.0 + {8a08d663-4995-40e3-b42c-3f910625f284} + Win32Proj + MouseJumpModuleInterface + MouseJump + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + $(SolutionDir)$(Platform)\$(Configuration)\modules\MouseUtils\ + PowerToys.MouseJump + + + true + + + false + + + + Level3 + Disabled + true + _DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + MultiThreadedDebug + stdcpplatest + + + Windows + true + $(OutDir)$(TargetName)$(TargetExt) + + + + + Level3 + MaxSpeed + true + true + true + NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + MultiThreaded + stdcpplatest + + + Windows + true + true + true + $(OutDir)$(TargetName)$(TargetExt) + + + + + $(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories) + + + + + Use + pch.h + + + + + + + + + + + Create + + + + + + {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/MouseUtils/MouseJump/dllmain.cpp b/src/modules/MouseUtils/MouseJump/dllmain.cpp new file mode 100644 index 000000000000..4be0abdc4818 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump/dllmain.cpp @@ -0,0 +1,302 @@ +#include "pch.h" + +#include +//#include +//#include +#include +#include "trace.h" +#include +#include + +extern "C" IMAGE_DOS_HEADER __ImageBase; + +HMODULE m_hModule; + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) +{ + m_hModule = hModule; + 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; +} + +// The PowerToy name that will be shown in the settings. +const static wchar_t* MODULE_NAME = L"MouseJump"; +// Add a description that will we shown in the module settings page. +const static wchar_t* MODULE_DESC = L"Quickly move the mouse long distances"; + +namespace +{ + const wchar_t JSON_KEY_PROPERTIES[] = L"properties"; + const wchar_t JSON_KEY_WIN[] = L"win"; + const wchar_t JSON_KEY_ALT[] = L"alt"; + const wchar_t JSON_KEY_CTRL[] = L"ctrl"; + const wchar_t JSON_KEY_SHIFT[] = L"shift"; + const wchar_t JSON_KEY_CODE[] = L"code"; + const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut"; +} + +// Implement the PowerToy Module Interface and all the required methods. +class MouseJump : public PowertoyModuleIface +{ +private: + // The PowerToy state. + bool m_enabled = false; + + // Hotkey to invoke the module + Hotkey m_hotkey; + HANDLE m_hProcess; + + void parse_hotkey(PowerToysSettings::PowerToyValues& settings) + { + auto settingsObject = settings.get_raw_json(); + if (settingsObject.GetView().Size()) + { + try + { + auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT); + m_hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN); + m_hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT); + m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); + m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); + m_hotkey.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); + } + catch (...) + { + Logger::error("Failed to initialize Mouse Jump start shortcut"); + } + } + else + { + Logger::info("MouseJump settings are empty"); + } + + if (!m_hotkey.key) + { + Logger::info("MouseJump is going to use default shortcut"); + m_hotkey.win = true; + m_hotkey.alt = false; + m_hotkey.shift = true; + m_hotkey.ctrl = false; + m_hotkey.key = 'D'; + } + } + + bool is_process_running() + { + return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT; + } + + void launch_process() + { + Logger::trace(L"Starting MouseJump process"); + unsigned long powertoys_pid = GetCurrentProcessId(); + + std::wstring executable_args = L""; + executable_args.append(std::to_wstring(powertoys_pid)); + + SHELLEXECUTEINFOW sei{ sizeof(sei) }; + sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI }; + sei.lpFile = L"modules\\MouseUtils\\MouseJumpUI\\PowerToys.MouseJumpUI.exe"; + sei.nShow = SW_SHOWNORMAL; + sei.lpParameters = executable_args.data(); + if (ShellExecuteExW(&sei)) + { + Logger::trace("Successfully started the Mouse Jump process"); + } + else + { + Logger::error(L"Mouse Jump failed to start. {}", get_last_error_or_default(GetLastError())); + } + + m_hProcess = sei.hProcess; + } + + // Load initial settings from the persisted values. + void init_settings(); + + void terminate_process() + { + TerminateProcess(m_hProcess, 1); + } + +public: + // Constructor + MouseJump() + { + LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mouseJumpLoggerName); + init_settings(); + }; + + ~MouseJump() + { + if (m_enabled) + { + terminate_process(); + } + m_enabled = false; + } + + // Destroy the powertoy and free memory + virtual void destroy() override + { + delete this; + } + + // Return the display name of the powertoy, this will be cached by the runner + virtual const wchar_t* get_name() override + { + return MODULE_NAME; + } + + // Return the non localized key of the powertoy, this will be cached by the runner + virtual const wchar_t* get_key() override + { + return MODULE_NAME; + } + + // Return the configured status for the gpo policy for the module + virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override + { + return powertoys_gpo::getConfiguredMouseJumpEnabledValue(); + } + + // Return JSON with the configuration options. + virtual bool get_config(wchar_t* buffer, int* buffer_size) override + { + HINSTANCE hinstance = reinterpret_cast(&__ImageBase); + + // Create a Settings object. + PowerToysSettings::Settings settings(hinstance, get_name()); + settings.set_description(MODULE_DESC); + + return settings.serialize_to_buffer(buffer, buffer_size); + } + + // Signal from the Settings editor to call a custom action. + // This can be used to spawn more complex editors. + virtual void call_custom_action(const wchar_t* action) override + { + } + + // Called by the runner to pass the updated settings values as a serialized JSON. + 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()); + + parse_hotkey(values); + + values.save_to_settings_file(); + } + catch (std::exception&) + { + // Improper JSON. + } + } + + // Enable the powertoy + virtual void enable() + { + m_enabled = true; + Trace::EnableJumpTool(true); + } + + // Disable the powertoy + virtual void disable() + { + if (m_enabled) + { + terminate_process(); + } + + m_enabled = false; + Trace::EnableJumpTool(false); + } + + // Returns if the powertoys is enabled + virtual bool is_enabled() override + { + return m_enabled; + } + + virtual bool on_hotkey(size_t /*hotkeyId*/) override + { + if (m_enabled) + { + Logger::trace(L"MouseJump hotkey pressed"); + Trace::InvokeJumpTool(); + if (is_process_running()) + { + terminate_process(); + } + launch_process(); + + return true; + } + + return false; + } + + virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override + { + if (m_hotkey.key) + { + if (hotkeys && buffer_size >= 1) + { + hotkeys[0] = m_hotkey; + } + + return 1; + } + else + { + return 0; + } + } + + // Returns whether the PowerToys should be enabled by default + virtual bool is_enabled_by_default() const override + { + return false; + } + +}; + +// Load the settings file. +void MouseJump::init_settings() +{ + try + { + // Load and parse the settings file for this PowerToy. + PowerToysSettings::PowerToyValues settings = + PowerToysSettings::PowerToyValues::load_from_settings_file(MouseJump::get_name()); + + parse_hotkey(settings); + + } + catch (std::exception&) + { + Logger::warn(L"An exception occurred while loading the settings file"); + // Error while loading from the settings file. Let default values stay as they are. + } +} + + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new MouseJump(); +} \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseJump/packages.config b/src/modules/MouseUtils/MouseJump/packages.config new file mode 100644 index 000000000000..48319b8c957e --- /dev/null +++ b/src/modules/MouseUtils/MouseJump/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseJump/pch.cpp b/src/modules/MouseUtils/MouseJump/pch.cpp new file mode 100644 index 000000000000..1d9f38c57d63 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/MouseUtils/MouseJump/pch.h b/src/modules/MouseUtils/MouseJump/pch.h new file mode 100644 index 000000000000..74abb62da1ea --- /dev/null +++ b/src/modules/MouseUtils/MouseJump/pch.h @@ -0,0 +1,10 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#include + +//#include +#include +#include +#include diff --git a/src/modules/MouseUtils/MouseJump/resource.h b/src/modules/MouseUtils/MouseJump/resource.h new file mode 100644 index 000000000000..67b823fd7f77 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump/resource.h @@ -0,0 +1,13 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by MeasureTool.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys MouseJump" +#define INTERNAL_NAME "PowerToys.MouseJump" +#define ORIGINAL_FILENAME "PowerToys.MouseJump.dll" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/MouseUtils/MouseJump/trace.cpp b/src/modules/MouseUtils/MouseJump/trace.cpp new file mode 100644 index 000000000000..9f5380284d67 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump/trace.cpp @@ -0,0 +1,38 @@ +#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::EnableJumpTool(const bool enabled) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "MouseJump_EnableJumpTool", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, "Enabled")); +} + +void Trace::InvokeJumpTool() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "MouseJump_InvokeJumpTool", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} diff --git a/src/modules/MouseUtils/MouseJump/trace.h b/src/modules/MouseUtils/MouseJump/trace.h new file mode 100644 index 000000000000..aaaa33629135 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump/trace.h @@ -0,0 +1,12 @@ +#pragma once + +class Trace +{ +public: + static void RegisterProvider(); + static void UnregisterProvider(); + + static void EnableJumpTool(const bool enabled) noexcept; + + static void InvokeJumpTool() noexcept; +}; diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/Helpers/LayoutHelperTests.cs b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Helpers/LayoutHelperTests.cs new file mode 100644 index 000000000000..64db5b4491f7 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI.UnitTests/Helpers/LayoutHelperTests.cs @@ -0,0 +1,514 @@ +// 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.Drawing; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MouseJumpUI.Helpers.Tests; + +[TestClass] +public static class LayoutHelperTests +{ + [TestClass] + public class CenterObjectTests + { + public class TestCase + { + public TestCase(Size obj, Point midpoint, Point expectedResult) + { + this.Obj = obj; + this.Midpoint = midpoint; + this.ExpectedResult = expectedResult; + } + + public Size Obj { get; set; } + + public Point Midpoint { get; set; } + + public Point ExpectedResult { get; set; } + } + + public static IEnumerable GetTestCases() + { + // zero-sized object should center exactly on the midpoint + yield return new[] { new TestCase(new(0, 0), new(0, 0), new(0, 0)), }; + + // odd-sized objects should center above/left of the midpoint + yield return new[] { new TestCase(new(1, 1), new(1, 1), new(0, 0)), }; + yield return new[] { new TestCase(new(1, 1), new(5, 5), new(4, 4)), }; + + // even-sized objects should center exactly on the midpoint + yield return new[] { new TestCase(new(2, 2), new(1, 1), new(0, 0)), }; + yield return new[] { new TestCase(new(2, 2), new(5, 5), new(4, 4)), }; + yield return new[] { new TestCase(new(800, 600), new(1000, 1000), new(600, 700)), }; + + // negative result position + yield return new[] { new TestCase(new(1000, 1200), new(300, 300), new(-200, -300)), }; + } + + [TestMethod] + [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + public void RunTestCases(TestCase data) + { + var actual = LayoutHelper.CenterObject(data.Obj, data.Midpoint); + var expected = data.ExpectedResult; + Assert.AreEqual(expected, actual); + } + } + + [TestClass] + public class CombineRegionsTests + { + public class TestCase + { + public TestCase(List bounds, Rectangle expectedResult) + { + this.Bounds = bounds; + this.ExpectedResult = expectedResult; + } + + public List Bounds { get; set; } + + public Rectangle ExpectedResult { get; set; } + } + + public static IEnumerable GetTestCases() + { + // empty list + yield return new[] + { + new TestCase( + new(), + Rectangle.Empty), + }; + + // empty bounds + yield return new[] + { + new TestCase( + new() + { + Rectangle.Empty, + }, + Rectangle.Empty), + }; + + // single region + // + // +---+ + // | 0 | + // +---+ + yield return new[] + { + new TestCase( + new() + { + new(100, 100, 100, 100), + }, + new(100, 100, 100, 100)), + }; + + // multi-monitor desktop + // + // +----------------+ + // | | + // | 1 +-------+ + // | | 0 | + // +----------------+-------+ + yield return new[] + { + new TestCase( + new() + { + new(5120, 0, 1920, 1080), + new(0, 0, 5120, 1440), + }, + new(0, 0, 7040, 1440)), + }; + + // multi-monitor desktop + // + // note - windows puts the *primary* monitor at the origin (0,0), + // so screens positioned *above* or *left* will have negative coordinates + // + // +-------+ + // | 0 | + // +-------+--------+ + // | | + // | 1 | + // | | + // +----------------+ + yield return new[] + { + new TestCase( + new() + { + new(0, -1000, 1920, 1080), + new(0, 0, 5120, 1440), + }, + new(0, -1000, 5120, 2440)), + }; + + // multi-monitor desktop + // + // note - windows puts the *primary* monitor at the origin (0,0), + // so screens positioned *above* or *left* will have negative coordinates + // + // +-------+----------------+ + // | 0 | | + // +-------+ 1 | + // | | + // +----------------+ + yield return new[] + { + new TestCase( + new() + { + new(-1920, 0, 1920, 1080), + new(0, 0, 5120, 1440), + }, + new(-1920, 0, 7040, 1440)), + }; + + // non-contiguous regions + // + // +---+ + // | 0 | +-------+ + // +---+ | | + // | 1 | + // | | + // +-------+ + yield return new[] + { + new TestCase( + new() + { + new(0, 0, 100, 100), + new(200, 150, 200, 200), + }, + new(0, 0, 400, 350)), + }; + } + + [TestMethod] + [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + public void RunTestCases(TestCase data) + { + var actual = LayoutHelper.CombineRegions(data.Bounds); + var expected = data.ExpectedResult; + Assert.AreEqual(expected, actual); + } + } + + [TestClass] + public class GetMidpointTests + { + } + + [TestClass] + public class MoveInsideTests + { + public class TestCase + { + public TestCase(Rectangle obj, Rectangle bounds, Rectangle expectedResult) + { + this.Obj = obj; + this.Bounds = bounds; + this.ExpectedResult = expectedResult; + } + + public Rectangle Obj { get; set; } + + public Rectangle Bounds { get; set; } + + public Rectangle ExpectedResult { get; set; } + } + + public static IEnumerable GetTestCases() + { + // already inside - obj fills bounds exactly + yield return new[] + { + new TestCase(new(0, 0, 100, 100), new(0, 0, 100, 100), new(0, 0, 100, 100)), + }; + + // already inside - obj exactly in each corner + yield return new[] + { + new TestCase(new(0, 0, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)), + }; + yield return new[] + { + new TestCase(new(100, 0, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)), + }; + yield return new[] + { + new TestCase(new(0, 100, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)), + }; + yield return new[] + { + new TestCase(new(100, 100, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)), + }; + + // move inside - obj outside each corner + yield return new[] + { + new TestCase(new(-50, -50, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)), + }; + yield return new[] + { + new TestCase(new(250, -50, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)), + }; + yield return new[] + { + new TestCase(new(-50, 250, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)), + }; + yield return new[] + { + new TestCase(new(150, 150, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)), + }; + } + + [TestMethod] + [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + public void RunTestCases(TestCase data) + { + var actual = LayoutHelper.MoveInside(data.Obj, data.Bounds); + var expected = data.ExpectedResult; + Assert.AreEqual(expected, actual); + } + } + + [TestClass] + public class ScaleLocationTests + { + } + + [TestClass] + public class ScaleToFitTests + { + public class TestCase + { + public TestCase(Size obj, Size bounds, Size expectedResult) + { + this.Obj = obj; + this.Bounds = bounds; + this.ExpectedResult = expectedResult; + } + + public Size Obj { get; set; } + + public Size Bounds { get; set; } + + public Size ExpectedResult { get; set; } + } + + public static IEnumerable GetTestCases() + { + // identity tests + yield return new[] + { + new TestCase(new(0, 0), new(0, 0), new(0, 0)), + }; + yield return new[] + { + new TestCase(new(512, 384), new(512, 384), new(512, 384)), + }; + yield return new[] + { + new TestCase(new(1024, 768), new(1024, 768), new(1024, 768)), + }; + + // integer scaling factor tests + yield return new[] + { + new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)), + }; + yield return new[] + { + new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768)), + }; + + // scale to fit width + yield return new[] + { + new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), + }; + + // scale to fit height + yield return new[] + { + new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)), + }; + } + + [TestMethod] + [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + public void RunTestCases(TestCase data) + { + var actual = LayoutHelper.ScaleToFit(data.Obj, data.Bounds); + var expected = data.ExpectedResult; + Assert.AreEqual(expected, actual); + } + } + + [TestClass] + public class GetPreviewFormBoundsTests + { + public class TestCase + { + public TestCase( + Rectangle desktopBounds, + Point cursorPosition, + Rectangle currentMonitorBounds, + Size maximumPreviewImageSize, + Size previewImagePadding, + Rectangle expectedResult) + { + this.DesktopBounds = desktopBounds; + this.CursorPosition = cursorPosition; + this.CurrentMonitorBounds = currentMonitorBounds; + this.MaximumPreviewImageSize = maximumPreviewImageSize; + this.PreviewImagePadding = previewImagePadding; + this.ExpectedResult = expectedResult; + } + + public Rectangle DesktopBounds { get; set; } + + public Point CursorPosition { get; set; } + + public Rectangle CurrentMonitorBounds { get; set; } + + public Size MaximumPreviewImageSize { get; set; } + + public Size PreviewImagePadding { get; set; } + + public Rectangle ExpectedResult { get; set; } + } + + public static IEnumerable GetTestCases() + { + // multi-monitor desktop + // + // +----------------+ + // | | + // | 1 +-------+ + // | | 0 | + // +----------------+-------+ + // + // clicked near top left corner so that the + // preview box overhangs the top and left + // + // +----------------+ + // | * | + // | 1 +-------+ + // | | 0 | + // +----------------+-------+ + // + // form is centered on mouse cursor and then + // nudged back into the top left corner + // + // +-----+----------+ + // | * | | + // +-----+ 1 +-------+ + // | | 0 | + // +----------------+-------+ + yield return new[] + { + new TestCase( + desktopBounds: new(-5120, -359, 7040, 1440), + cursorPosition: new(-5020, -259), + currentMonitorBounds: new(-5120, -359, 5120, 1440), + maximumPreviewImageSize: new(1600, 1200), + previewImagePadding: new(10, 10), + expectedResult: new(-5120, -359, 1610, 337)), + }; + + // multi-monitor desktop + // + // +----------------+ + // | | + // | 1 +-------+ + // | | 0 | + // +----------------+-------+ + // + // clicked in the center of the second monitor + // + // +----------------+ + // | | + // | * +-------+ + // | | 0 | + // +----------------+-------+ + // + // form is centered on the mouse cursor + // + // +----------------+ + // | +-----+ | + // | | * | +-------+ + // | +-----+ | 0 | + // +----------------+-------+ + yield return new[] + { + new TestCase( + desktopBounds: new(-5120, -359, 7040, 1440), + cursorPosition: new(-2560, 361), + currentMonitorBounds: new(-5120, -359, 5120, 1440), + maximumPreviewImageSize: new(1600, 1200), + previewImagePadding: new(10, 10), + expectedResult: new(-3365, 192, 1610, 337)), + }; + + // multi-monitor desktop + // + // +----------------+ + // | | + // | 1 +-------+ + // | | 0 | + // +----------------+-------+ + // + // clicked in the center of the monitor + // + // +----------------+ + // | | + // | * +-------+ + // | | 0 | + // +----------------+-------+ + // + // max preview is larger than monitor, + // form is scaled to monitor size, with + // consideration for image padding + // + // *----------------* + // |+--------------+| + // || * |+-------+ + // |+--------------+| 0 | + // +----------------+-------+ + yield return new[] + { + new TestCase( + desktopBounds: new(-5120, -359, 7040, 1440), + cursorPosition: new(-2560, 361), + currentMonitorBounds: new(-5120, -359, 5120, 1440), + maximumPreviewImageSize: new(160000, 120000), + previewImagePadding: new(10, 10), + expectedResult: new(-5120, -166, 5120, 1055)), + }; + } + + [TestMethod] + [DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)] + public void RunTestCases(TestCase data) + { + var actual = LayoutHelper.GetPreviewFormBounds( + desktopBounds: data.DesktopBounds, + activatedPosition: data.CursorPosition, + activatedMonitorBounds: data.CurrentMonitorBounds, + maximumThumbnailImageSize: data.MaximumPreviewImageSize, + thumbnailImagePadding: data.PreviewImagePadding); + var expected = data.ExpectedResult; + Assert.AreEqual(expected, actual); + } + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI.UnitTests/MouseJumpUI.UnitTests.csproj b/src/modules/MouseUtils/MouseJumpUI.UnitTests/MouseJumpUI.UnitTests.csproj new file mode 100644 index 000000000000..67886dc27223 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI.UnitTests/MouseJumpUI.UnitTests.csproj @@ -0,0 +1,29 @@ + + + + + net7.0-windows10.0.19041.0 + {D9C5DE64-6849-4278-91AD-9660AECF2876} + Microsoft.MouseJumpUI.UnitTests + false + enable + Library + $(Version).0 + + + + ..\..\..\..\$(Platform)\$(Configuration)\modules\MouseUtils\MouseJumpUI.UnitTests\ + + + + + + + + + + + + + + diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/LayoutHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/LayoutHelper.cs new file mode 100644 index 000000000000..3fafa63c3c16 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Helpers/LayoutHelper.cs @@ -0,0 +1,191 @@ +// 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.Drawing; +using System.Linq; + +namespace MouseJumpUI.Helpers; + +internal static class LayoutHelper +{ + /// + /// Center an object on the given origin. + /// + public static Point CenterObject(Size obj, Point origin) + { + return new Point( + x: (int)(origin.X - ((float)obj.Width / 2)), + y: (int)(origin.Y - ((float)obj.Height / 2))); + } + + /// + /// Combines the specified regions and returns the smallest rectangle that contains them. + /// + /// The regions to combine. + /// + /// Returns the smallest rectangle that contains all the specified regions. + /// + public static Rectangle CombineRegions(List regions) + { + if (regions == null) + { + throw new ArgumentNullException(nameof(regions)); + } + + if (regions.Count == 0) + { + return Rectangle.Empty; + } + + var combined = regions.Aggregate( + seed: regions[0], + func: Rectangle.Union); + return combined; + } + + /// + /// Returns the midpoint of the given region. + /// + public static Point GetMidpoint(Rectangle region) + { + return new Point( + (region.Left + region.Right) / 2, + (region.Top + region.Bottom) / 2); + } + + /// + /// Returns the largest Size object that can fit inside + /// all of the given sizes. (Equivalent to a Size + /// object with the smallest Width and smallest Height from + /// all of the specified sizes). + /// + public static Size IntersectSizes(params Size[] sizes) + { + return new Size( + sizes.Min(s => s.Width), + sizes.Min(s => s.Height)); + } + + /// + /// Returns the location to move the inner rectangle so that it sits entirely inside + /// the outer rectangle. Returns the inner rectangle's current position if it is + /// already inside the outer rectangle. + /// + public static Rectangle MoveInside(Rectangle inner, Rectangle outer) + { + if ((inner.Width > outer.Width) || (inner.Height > outer.Height)) + { + throw new ArgumentException($"{nameof(inner)} cannot be larger than {nameof(outer)}."); + } + + return inner with + { + X = Math.Clamp(inner.X, outer.X, outer.Right - inner.Width), + Y = Math.Clamp(inner.Y, outer.Y, outer.Bottom - inner.Height), + }; + } + + /// + /// Scales a location within a reference region onto a new region + /// so that it's proportionally in the same position in the new region. + /// + public static Point ScaleLocation(Rectangle originalBounds, Point originalLocation, Rectangle scaledBounds) + { + return new Point( + (int)(originalLocation.X / (double)originalBounds.Width * scaledBounds.Width) + scaledBounds.Left, + (int)(originalLocation.Y / (double)originalBounds.Height * scaledBounds.Height) + scaledBounds.Top); + } + + /// + /// Scale an object to fit inside the specified bounds while maintaining aspect ratio. + /// + public static Size ScaleToFit(Size obj, Size bounds) + { + if (bounds.Width == 0 || bounds.Height == 0) + { + return Size.Empty; + } + + var widthRatio = (double)obj.Width / bounds.Width; + var heightRatio = (double)obj.Height / bounds.Height; + var scaledSize = (widthRatio > heightRatio) + ? bounds with + { + Height = (int)(obj.Height / widthRatio), + } + : bounds with + { + Width = (int)(obj.Width / heightRatio), + }; + return scaledSize; + } + + /// + /// Calculates the position to show the preview form based on a number of factors. + /// + /// + /// The bounds of the entire desktop / virtual screen. Might start at a negative + /// x, y if a non-primary screen is located left of or above the primary screen. + /// + /// + /// The current position of the cursor on the virtual desktop. + /// + /// + /// The bounds of the screen the cursor is currently on. Might start at a negative + /// x, y if a non-primary screen is located left of or above the primary screen. + /// + /// + /// The largest allowable size of the preview image. This is literally the just + /// image itself, not including padding around the image. + /// + /// + /// The total width and height of padding around the preview image. + /// + /// + /// The size and location to use when showing the preview image form. + /// + public static Rectangle GetPreviewFormBounds( + Rectangle desktopBounds, + Point activatedPosition, + Rectangle activatedMonitorBounds, + Size maximumThumbnailImageSize, + Size thumbnailImagePadding) + { + // see https://learn.microsoft.com/en-gb/windows/win32/gdi/the-virtual-screen + // calculate the maximum size the form is allowed to be + var maxFormSize = LayoutHelper.IntersectSizes( + new[] + { + // can't be bigger than the current screen + activatedMonitorBounds.Size, + + // can't be bigger than the max preview image + // *plus* the padding around the preview image + // (max thumbnail image size doesn't include the padding) + maximumThumbnailImageSize + thumbnailImagePadding, + }); + + // calculate the actual form size by scaling the entire + // desktop bounds into the max thumbnail size while accounting + // for the size of the padding around the preview + var thumbnailImageSize = LayoutHelper.ScaleToFit( + obj: desktopBounds.Size, + bounds: maxFormSize - thumbnailImagePadding); + var formSize = thumbnailImageSize + thumbnailImagePadding; + + // center the form to the activated position, but nudge it back + // inside the visible area of the screen if it falls outside + var formBounds = LayoutHelper.MoveInside( + inner: new Rectangle( + LayoutHelper.CenterObject( + obj: formSize, + origin: activatedPosition), + formSize), + outer: activatedMonitorBounds); + + return formBounds; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/Logger.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/Logger.cs new file mode 100644 index 000000000000..a1d4d0f9af3c --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Helpers/Logger.cs @@ -0,0 +1,77 @@ +// 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; + +namespace MouseJumpUI.Helpers +{ + // TODO: use centralized logger https://github.com/microsoft/PowerToys/issues/19650 + public static class Logger + { + private static readonly string ApplicationLogPath = Path.Combine(interop.Constants.AppDataPath(), "MouseJump\\Logs"); + + static Logger() + { + if (!Directory.Exists(ApplicationLogPath)) + { + Directory.CreateDirectory(ApplicationLogPath); + } + + // Using InvariantCulture since this is used for a log file name + var logFilePath = 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/MouseUtils/MouseJumpUI/MainForm.Designer.cs b/src/modules/MouseUtils/MouseJumpUI/MainForm.Designer.cs new file mode 100644 index 000000000000..073ffbf468df --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/MainForm.Designer.cs @@ -0,0 +1,95 @@ +// 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.Forms; + +namespace MouseJumpUI; + +partial class MainForm +{ + + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); + panel1 = new Panel(); + Thumbnail = new PictureBox(); + panel1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)Thumbnail).BeginInit(); + SuspendLayout(); + // + // panel1 + // + panel1.BackColor = System.Drawing.SystemColors.Highlight; + panel1.Controls.Add(Thumbnail); + panel1.Dock = DockStyle.Fill; + panel1.Location = new System.Drawing.Point(0, 0); + panel1.Name = "panel1"; + panel1.Padding = new Padding(5); + panel1.Size = new System.Drawing.Size(800, 450); + panel1.TabIndex = 1; + // + // Thumbnail + // + Thumbnail.BackColor = System.Drawing.SystemColors.Control; + Thumbnail.Dock = DockStyle.Fill; + Thumbnail.Location = new System.Drawing.Point(5, 5); + Thumbnail.Name = "Thumbnail"; + Thumbnail.Size = new System.Drawing.Size(790, 440); + Thumbnail.SizeMode = PictureBoxSizeMode.StretchImage; + Thumbnail.TabIndex = 1; + Thumbnail.TabStop = false; + Thumbnail.Click += Thumbnail_Click; + // + // MainForm + // + AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new System.Drawing.Size(800, 450); + Controls.Add(panel1); + FormBorderStyle = FormBorderStyle.None; + Icon = (System.Drawing.Icon)resources.GetObject("$this.Icon"); + KeyPreview = true; + Name = "MainForm"; + ShowInTaskbar = false; + StartPosition = FormStartPosition.Manual; + Text = "MouseJump"; + TopMost = true; + Deactivate += MainForm_Deactivate; + Load += MainForm_Load; + KeyDown += MainForm_KeyDown; + panel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)Thumbnail).EndInit(); + ResumeLayout(false); + } + + #endregion + + private Panel panel1; + private PictureBox Thumbnail; + +} diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs new file mode 100644 index 000000000000..cfd0836ee47b --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs @@ -0,0 +1,177 @@ +// 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.Drawing; +using System.Drawing.Imaging; +using System.Linq; +using System.Windows.Forms; +using MouseJumpUI.Helpers; + +namespace MouseJumpUI; + +internal partial class MainForm : Form +{ + public MainForm() + { + this.InitializeComponent(); + this.ShowThumbnail(); + } + + private void MainForm_Load(object sender, EventArgs e) + { + } + + private void MainForm_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Escape) + { + this.OnDeactivate(EventArgs.Empty); + } + } + + private void MainForm_Deactivate(object sender, EventArgs e) + { + // dispose the existing image if there is one + if (Thumbnail.Image != null) + { + Thumbnail.Image.Dispose(); + Thumbnail.Image = null; + } + + this.Close(); + } + + private void Thumbnail_Click(object sender, EventArgs e) + { + var mouseEventArgs = (MouseEventArgs)e; + Logger.LogInfo($"Reporting mouse event args \n\tbutton = {mouseEventArgs.Button}\n\tlocation = {mouseEventArgs.Location} "); + + if (mouseEventArgs.Button == MouseButtons.Left) + { + // plain click - move mouse pointer + var desktopBounds = LayoutHelper.CombineRegions( + Screen.AllScreens.Select( + screen => screen.Bounds).ToList()); + Logger.LogInfo($"desktop bounds = {desktopBounds}"); + + var mouseEvent = (MouseEventArgs)e; + + var scaledLocation = LayoutHelper.ScaleLocation( + originalBounds: Thumbnail.Bounds, + originalLocation: new Point(mouseEvent.X, mouseEvent.Y), + scaledBounds: desktopBounds); + Logger.LogInfo($"scaled location = {scaledLocation}"); + + // set the new cursor position *twice* - the cursor sometimes end up in + // the wrong place if we try to cross the dead space between non-aligned + // monitors - e.g. when trying to move the cursor from (a) to (b) we can + // *sometimes* - for no clear reason - end up at (c) instead. + // + // +----------------+ + // |(c) (b) | + // | | + // | | + // | | + // +---------+ | + // | (a) | | + // +---------+----------------+ + // + // setting the position a second time seems to fix this and moves the + // cursor to the expected location (b) - for more details see + // https://github.com/mikeclayton/FancyMouse/pull/3 + Cursor.Position = scaledLocation; + Cursor.Position = scaledLocation; + Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent()); + } + + this.Close(); + } + + public void ShowThumbnail() + { + if (this.Thumbnail.Image != null) + { + var tmp = this.Thumbnail.Image; + this.Thumbnail.Image = null; + tmp.Dispose(); + } + + var screens = Screen.AllScreens; + foreach (var i in Enumerable.Range(0, screens.Length)) + { + var screen = screens[i]; + Logger.LogInfo($"screen[{i}] = \"{screen.DeviceName}\"\n\tprimary = {screen.Primary}\n\tbounds = {screen.Bounds}\n\tworking area = {screen.WorkingArea}"); + } + + var desktopBounds = LayoutHelper.CombineRegions( + screens.Select(screen => screen.Bounds).ToList()); + Logger.LogInfo( + $"desktop bounds = {desktopBounds}"); + + var activatedPosition = Cursor.Position; + Logger.LogInfo( + $"activated position = {activatedPosition}"); + + var previewImagePadding = new Size( + panel1.Padding.Left + panel1.Padding.Right, + panel1.Padding.Top + panel1.Padding.Bottom); + Logger.LogInfo( + $"image padding = {previewImagePadding}"); + + var maxThumbnailSize = new Size(1600, 1200); + var formBounds = LayoutHelper.GetPreviewFormBounds( + desktopBounds: desktopBounds, + activatedPosition: activatedPosition, + activatedMonitorBounds: Screen.FromPoint(activatedPosition).Bounds, + maximumThumbnailImageSize: maxThumbnailSize, + thumbnailImagePadding: previewImagePadding); + Logger.LogInfo( + $"form bounds = {formBounds}"); + + // take a screenshot of the entire desktop + // see https://learn.microsoft.com/en-gb/windows/win32/gdi/the-virtual-screen + var screenshot = new Bitmap(desktopBounds.Width, desktopBounds.Height, PixelFormat.Format32bppArgb); + using (var graphics = Graphics.FromImage(screenshot)) + { + // note - it *might* be faster to capture each monitor individually and assemble them into + // a single image ourselves as we *may* not have to transfer all of the blank pixels + // that are outside the desktop bounds - e.g. the *** in the ascii art below + // + // +----------------+******** + // | |******** + // | 1 +-------+ + // | | | + // +----------------+ 0 | + // *****************| | + // *****************+-------+ + // + // for very irregular monitor layouts this *might* be a big percentage of the rectangle + // containing the desktop bounds. + // + // then again, it might not make much difference at all - we'd need to do some perf tests + graphics.CopyFromScreen(desktopBounds.Left, desktopBounds.Top, 0, 0, desktopBounds.Size); + } + + // resize and position the form + // note - do this in two steps rather than "this.Bounds = formBounds" as there + // appears to be an issue in WinForms with dpi scaling even when using PerMonitorV2, + // where the form scaling uses either the *primary* screen scaling or the *previous* + // screen's scaling when the form is moved to a different screen. i've got no idea + // *why*, but the exact sequence of calls below seems to be a workaround... + // see https://github.com/mikeclayton/FancyMouse/issues/2 + this.Location = formBounds.Location; + _ = this.PointToScreen(Point.Empty); + this.Size = formBounds.Size; + + // update the preview image + this.Thumbnail.Image = screenshot; + + this.Show(); + Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent()); + + // we have to activate the form to make sure the deactivate event fires + this.Activate(); + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.resx b/src/modules/MouseUtils/MouseJumpUI/MainForm.resx new file mode 100644 index 000000000000..e33d2ec3e8ae --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/MainForm.resx @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAA + AABgAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAD/AAAA/wAAAP8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAP9kdJz/ZHSc/2R0nP9kdJz/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAA/2R0nP9kdJz/ZHSc/2R0nP9kdJz/ZHSc/wAAAP8AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAA/2R0nP9kdJz/ZHSc/2R0nP9kdJz/ZHSc/wAA + AP8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAAAP9kdJz/ZHSc/2R0 + nP9kdJz/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/xuLj/8bi + 4/8AAAD/AAAA/wAAAP8AAAD/xuLj/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi + 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AP/G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi + 4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAA/8bi4//G4uP/xuLj/5Okpf8kISD/k6Sl/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/5Ok + pf8kISD/k6Sl/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/xuLj/yQhIP8kISD/JCEg/8bi4//G4uP/xuLj/8bi + 4//G4uP/xuLj/yQhIP8kISD/JCEg/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4//G4uP/xuLj/5Okpf8kISD/k6Sl/8bi + 4//G4uP/xuLj/8bi4//G4uP/xuLj/5Okpf8kISD/k6Sl/8bi4//G4uP/xuLj/8bi4/8AAAD/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4//G4uP/xuLj/8bi + 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi + 4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAA + AP8AAAD/AAAA/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/wAA + AP8AAAD/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi + 4//G4uP/AAAA/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi + 4//G4uP/AAAA/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAD/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4//G4uP/xuLj/8bi + 4//G4uP/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4/8AAAD/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAD/xuLj/wAAAP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi + 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf+AmLX/AAAA/8bi + 4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAAAP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CY + tf8AAAD/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CY + tf+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/4CY + tf+AmLX/gJi1/wAAAP/G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/AAAA/4CY + tf+AmLX/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4/8AAAD/gJi1/4CY + tf+AmLX/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/AAAA/wAAAP/G4uP/xuLj/8bi4//G4uP/AAAA/wAA + AP/G4uP/AAAA/4CYtf+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/wAAAP8AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi + 4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAD/AAAA/wAA + AP8AAAD/AAAAAAAAAP/G4uP/xuLj/wAAAP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/wAA + AP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAA/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4/8AAAD/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CY + tf8AAAD/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/wAA + AP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CY + tf+AmLX/gJi1/4CYtf8AAAD/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/wAA + AP/G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AP/G4uP/AAAA/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAA + AP+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAAAP+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAD/xuLj/wAAAP+AmLX/AAAA/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/wAAAP+AmLX/AAAA/8bi4/8AAAD/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4/8AAAD/xuLj/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4/8AAAD/xuLj/8bi + 4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi + 4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAD/AAAA/wAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAP8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAP///////wAA////////AAD///////8AAP///////wAA////////AAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA///8P///AAD///gf//8AAP//8A///wAA///gB/// + AAD//8AD//8AAP//gAH//wAA//8AAP//AAD//gAAf/8AAP/8AAA//wAA//wAAD//AAD/+AAAH/8AAP/4 + AAAf/wAA//AAAA//AAD/4AAAB/8AAP/AAAAD/wAA/8AAAAP/AAD/gAAAAf8AAP8AAAAA/wAA/wAAAAD/ + AAD/AAQgAP8AAP8AD/AA/wAA/wAf+AD/AAD/AD/8AP8AAP+AP/wB/wAA/8B//gP/AAD/wH/+A/8AAP/g + //8H/wAA//H//4//AAD///////8AAP///////wAA////////AAD///////8AAP///////wAA//////// + AAD///////8AAP///////wAA////////AAD///////8AACgAAAAgAAAAQAAAAAEAIAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAA/wAAAP8AAAD/AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/2R0nP9kdJz/ZHSc/2R0nP8AAAD/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP9kdJz/ZHSc/2R0nP9kdJz/ZHSc/2R0 + nP8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAA/2R0nP9kdJz/ZHSc/2R0 + nP9kdJz/ZHSc/wAAAP8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/2R0 + nP9kdJz/ZHSc/2R0nP8AAAD/xuLj/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/8bi + 4//G4uP/AAAA/wAAAP8AAAD/AAAA/8bi4//G4uP/xuLj/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi + 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi + 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi + 4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AP/G4uP/xuLj/8bi4/+TpKX/JCEg/5Okpf/G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/+TpKX/JCEg/5Ok + pf/G4uP/xuLj/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAA/8bi4//G4uP/xuLj/yQhIP8kISD/JCEg/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/yQh + IP8kISD/JCEg/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAP/G4uP/xuLj/8bi4//G4uP/k6Sl/yQhIP+TpKX/xuLj/8bi4//G4uP/xuLj/8bi + 4//G4uP/k6Sl/yQhIP+TpKX/xuLj/8bi4//G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi + 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAAAP8AAAD/AAAA/8bi4//G4uP/xuLj/8bi + 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/wAAAP8AAAD/AAAA/8bi4//G4uP/AAAA/wAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/AAAA/8bi + 4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/AAAA/8bi + 4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/4CY + tf+AmLX/AAAA/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/4CY + tf+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAD/xuLj/wAAAP+AmLX/gJi1/4CY + tf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi4/8AAAD/gJi1/4CY + tf+AmLX/gJi1/4CYtf+AmLX/AAAA/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CY + tf+AmLX/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/wAA + AP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4/8AAAD/AAAAAAAAAP/G4uP/xuLj/wAA + AP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4//G4uP/xuLj/8bi4//G4uP/xuLj/8bi + 4//G4uP/xuLj/wAAAP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4/8AAAD/AAAA/8bi + 4/8AAAD/gJi1/4CYtf+AmLX/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/AAAA/wAAAP/G4uP/xuLj/8bi + 4//G4uP/AAAA/wAAAP/G4uP/AAAA/4CYtf+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/wAA + AP8AAAD/xuLj/wAAAP+AmLX/gJi1/4CYtf+AmLX/gJi1/4CYtf8AAAD/xuLj/8bi4/8AAAD/AAAAAAAA + AP8AAAD/AAAA/wAAAP8AAAAAAAAA/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/4CYtf+AmLX/gJi1/wAA + AP/G4uP/AAAA/wAAAP/G4uP/AAAA/4CYtf+AmLX/gJi1/4CYtf+AmLX/AAAA/8bi4//G4uP/AAAA/wAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CYtf+AmLX/gJi1/4CY + tf+AmLX/AAAA/8bi4/8AAAD/AAAA/8bi4/8AAAD/gJi1/4CYtf+AmLX/gJi1/wAAAP/G4uP/xuLj/wAA + AP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi4//G4uP/AAAA/4CY + tf+AmLX/gJi1/4CYtf8AAAD/xuLj/wAAAP8AAAD/xuLj/8bi4/8AAAD/gJi1/4CYtf+AmLX/AAAA/8bi + 4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8bi + 4/8AAAD/gJi1/4CYtf+AmLX/AAAA/8bi4//G4uP/AAAA/wAAAAAAAAD/xuLj/8bi4/8AAAD/gJi1/wAA + AP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAD/xuLj/8bi4/8AAAD/gJi1/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAD/xuLj/wAA + AP+AmLX/AAAA/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAD/xuLj/wAAAP+AmLX/AAAA/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAA + AP/G4uP/xuLj/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/wAAAP/G4uP/xuLj/wAAAP8AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAP/G4uP/xuLj/8bi4/8AAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/G4uP/xuLj/8bi4/8AAAD/AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAD/AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAD/AAAA/wAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////////////D////gf///wD///4Af//8AD//+A + Af//AAD//gAAf/wAAD/8AAA/+AAAH/gAAB/wAAAP4AAAB8AAAAPAAAADgAAAAQAAAAAAAAAAAAQgAAAP + 8AAAH/gAAD/8AIA//AHAf/4DwH/+A+D//wfx//+P//////////8oAAAAEAAAACAAAAABACAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAyOk7/MjpO/wAA + AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAyOk7/ZHSc/2R0 + nP8yOk7/AAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECVqqr/laqq/zI6 + Tv8yOk7/laqq/5Wqqv8AAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECVqqr/xuLj/8bi + 4//G4uP/xuLj/8bi4//G4uP/laqq/wAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABjcXL/xuLj/0BC + Qf+RoqP/xuLj/8bi4/+RoqP/QEJB/8bi4/9jcXL/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAxuLj/8bi + 4/+RoqP/udPU/8bi4//G4uP/udPU/5Gio//G4uP/xuLj/wAAAIAAAAAAAAAAAAAAAAAAAABAlaqq/1Jf + Zv9ATFv/laqq/8bi4//G4uP/xuLj/8bi4/+Vqqr/QExb/1JfZv+Vqqr/AAAAQAAAAAAAAAAAY3Fy/1Jf + Zv+AmLX/gJi1/0BMW//G4uP/xuLj/8bi4//G4uP/QExb/4CYtf+AmLX/Ul9m/2Nxcv8AAAAAQktMv5Wq + qv9gcoj/gJi1/4CYtf9SX2b/xuLj/8bi4//G4uP/xuLj/1JfZv+AmLX/gJi1/2ByiP+Vqqr/QktMv2Nx + cv9ATFv/gJi1/4CYtf9gcoj/laqq/wAAAL9jcXL/Y3Fy/wAAAL+Vqqr/YHKI/4CYtf+AmLX/QExb/2Nx + cv9jcXL/QExb/4CYtf9gcoj/laqq/0JLTL8AAAAAAAAAAAAAAAAAAAAAQktMv5Wqqv9gcoj/gJi1/0BM + W/9jcXL/QktMv5Wqqv9gcoj/Ul9m/2Nxcv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABjcXL/Ul9m/2By + iP+Vqqr/QktMvwAAAABjcXL/Ul9m/5Wqqv8AAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgJWq + qv9SX2b/Y3Fy/wAAAAAAAAAAAAAAQGNxcv9CS0y/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AABCS0y/Y3Fy/wAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//rEH8P6xB+B+sQfAPrEHgB6xB4AesQcADrEGAAaxBgAGsQQAA + rEEAAKxBA8CsQQfgrEGH4axBj/GsQf//rEE= + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj b/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj new file mode 100644 index 000000000000..0c6b5b90e314 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj @@ -0,0 +1,66 @@ + + + + PowerToys.MouseJumpUI + PowerToys MouseJumpUI + $(Version).0 + ..\..\..\..\$(Platform)\$(Configuration)\modules\MouseUtils\MouseJumpUI + false + false + true + true + MouseJumpUI.Program + true + true + + + + + win10-x64 + + + win10-arm64 + + + + PerMonitorV2 + + + + {D962A009-834F-4EEC-AABB-430DF8F98E39} + WinExe + MouseJumpUI + PowerToys.MouseJumpUI + net7.0-windows10.0.19041.0 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + true + + + + + PowerToys.GPOWrapper + $(OutDir) + false + + + + true + DEBUG;TRACE + full + false + + + TRACE + pdbonly + prompt + true + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseJumpUI/Program.cs b/src/modules/MouseUtils/MouseJumpUI/Program.cs new file mode 100644 index 000000000000..6770a0a7be2b --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Program.cs @@ -0,0 +1,38 @@ +// 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.Forms; +using MouseJumpUI.Helpers; + +namespace MouseJumpUI; + +internal static class Program +{ + /// + /// The main entry point for the application. + /// + [STAThread] + private static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + + if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredMouseJumpEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled) + { + // TODO : Log message + Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator."); + return; + } + + if (Application.HighDpiMode != HighDpiMode.PerMonitorV2) + { + Logger.LogError("High dpi mode is not set to PerMonitorV2."); + return; + } + + Application.Run(new MainForm()); + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Properties/AssemblyInfo.cs b/src/modules/MouseUtils/MouseJumpUI/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..b44c65eead42 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// 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.CompilerServices; + +[assembly: InternalsVisibleTo("MouseJumpUI.UnitTests")] diff --git a/src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpShowEvent.cs b/src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpShowEvent.cs new file mode 100644 index 000000000000..7abf856d40d9 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpShowEvent.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 MouseJumpUI.Telemetry +{ + [EventData] + public class MouseJumpShowEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpTeleportCursorEvent.cs b/src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpTeleportCursorEvent.cs new file mode 100644 index 000000000000..40170b0d6968 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Telemetry/MouseJumpTeleportCursorEvent.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 MouseJumpUI.Telemetry +{ + [EventData] + public class MouseJumpTeleportCursorEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +} diff --git a/src/runner/main.cpp b/src/runner/main.cpp index bedcba612fe4..3e0ed200cc63 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -158,6 +158,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"modules/Awake/PowerToys.AwakeModuleInterface.dll", L"modules/MouseUtils/PowerToys.FindMyMouse.dll", L"modules/MouseUtils/PowerToys.MouseHighlighter.dll", + L"modules/MouseUtils/PowerToys.MouseJump.dll", L"modules/AlwaysOnTop/PowerToys.AlwaysOnTopModuleInterface.dll", L"modules/MouseUtils/PowerToys.MousePointerCrosshairs.dll", L"modules/PowerAccent/PowerToys.PowerAccentModuleInterface.dll", diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs index f68735ba3e57..c501ada3f3c9 100644 --- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs @@ -214,6 +214,22 @@ public bool MouseHighlighter } } + private bool mouseJump = true; + + [JsonPropertyName("MouseJump")] + public bool MouseJump + { + get => mouseJump; + set + { + if (mouseJump != value) + { + LogTelemetryEvent(value); + mouseJump = value; + } + } + } + private bool alwaysOnTop = true; [JsonPropertyName("AlwaysOnTop")] diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs b/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs new file mode 100644 index 000000000000..1d5a9ad480ff --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/MouseJumpProperties.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class MouseJumpProperties + { + [JsonPropertyName("activation_shortcut")] + public HotkeySettings ActivationShortcut { get; set; } + + public MouseJumpProperties() + { + ActivationShortcut = new HotkeySettings(true, false, false, true, 0x44); + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs new file mode 100644 index 000000000000..73b15f508637 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.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 MouseJumpSettings : BasePTModuleSettings, ISettingsConfig + { + public const string ModuleName = "MouseJump"; + + [JsonPropertyName("properties")] + public MouseJumpProperties Properties { get; set; } + + public MouseJumpSettings() + { + Name = ModuleName; + Properties = new MouseJumpProperties(); + Version = "1.0"; + } + + public string GetModuleName() + { + return Name; + } + + // This can be utilized in the future if the settings.json file is to be modified/deleted. + public bool UpgradeSettingsConfiguration() + { + return false; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/SndMouseJumpSettings.cs b/src/settings-ui/Settings.UI.Library/SndMouseJumpSettings.cs new file mode 100644 index 000000000000..a19c4a074933 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/SndMouseJumpSettings.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 SndMouseJumpSettings + { + [JsonPropertyName("MouseJump")] + public MouseJumpSettings MouseJump { get; set; } + + public SndMouseJumpSettings() + { + } + + public SndMouseJumpSettings(MouseJumpSettings settings) + { + MouseJump = settings; + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsMouseJump.png b/src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsMouseJump.png new file mode 100644 index 0000000000000000000000000000000000000000..15568110cc985546a726ba335f3bc4ffff93625e GIT binary patch literal 1510 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1$#+EK~z{r&6xd9 zQ&$+rPw#DSZ!HvPq4>g}y!aAavPeea=G081G1;cuV9td=Vodne7?zl2nf?P?;v31r zOQK=c>Hj6b-^9Ggf5$BP1jD{X0^?QQSvp6B$Ifh+e`Gm&p{IM4H(L;3Wa z?>W6xLqNNB?J`2Q9-0*zgSADJ1Z@J^UAx^rt&w52LeoNfnF>Su30DJG=4Hd6kYPOn z;WjN5SrKTlJ)w3V zTDk2txvZ+VKo$EwEbi#&IQ%S7E>pgxr=YAvPDzkU&5tlFUMXZey{J-TIF=5mNO8*& zg&JJ7qv(Ea>9MI~Tg83b2ivih`|$Ec&0LS|zuG^FOX+B|%&< zaqDY32^7?uH>(+35hxOYH>S}LpAVr#MW{^YZ$oBxZ;hOi0B_lje#(0Ma}OVIUYE1s?FuKq0IMwPvvO8vOBVaysj$XEC@6P-M2@bBbpQpdNu=3ByXk@=4c^5C2DX5AcTMgfh~&xQ79O3Px*-$4w2@w zXFe@0E!`NM|F6(bM*sK^a*FSYL4%vT!|l-$ye`9~$>BVy0L_QNCW;~*W3@WM^9FL@ zz=6-Is;cN^7?XF|O4AIVu|-Zb5~EE*BYTrx*(ukECL;ReaGn$%%m6@&LeNa^Efxzo zdGh4>mX?;AEX!uNUd8dX8 zPe)%Qj^Y9tQZhKWXUM5jr;c5^bm<1xnwpxv#~xQ{e0-d4TWV_R+gL+6@=EvP5vK_p zdD|oVmw2*9GvBBLNP07Kg69|8$ki@7X92|p3IqaNU0vPjD_5@kD3=KMP_`*rEAo-s#m2^e|VS~>y7vPV1P8hNhRf`WqRFU(PQ zclVIP;kYNKv+C;V@|KsEZv|lDcDqSoVWGLRv(xMI`TEjbcl&f9->oof@D@I3Y@6n- z#^F`lKZi?DkH4EVJ#xV|(t(FSQE^uYj(GHpKPA|%+ZQihyo;-ZCl0su$dM!a0FcLV zybxdQJM%5Z{kA<|1AbQXKQO~zW{&zEf3EsWAcxCe9(C#q)qaz z9k^hZ%c{cv?RU#@!h$<#EnJ#GqtQ4hm;N_B^Om6u27}{8z@VNB=tT;F@cWsiA_;+> zMB^o)CkWI7Eqe97Tp>db+uz?Wz|nsU$uhaoulW{Hv9I)Q+wFGmKcv-=_i{%Ti2wiq M07*qoM6N<$f~R%Q3;+NC literal 0 HcmV?d00001 diff --git a/src/settings-ui/Settings.UI/MainWindow.xaml.cs b/src/settings-ui/Settings.UI/MainWindow.xaml.cs index f4b9e2746a0b..06ce02f20d84 100644 --- a/src/settings-ui/Settings.UI/MainWindow.xaml.cs +++ b/src/settings-ui/Settings.UI/MainWindow.xaml.cs @@ -120,6 +120,9 @@ public MainWindow(bool isDark, bool createHidden = false) case "MouseHighlighter": needToUpdate = generalSettingsConfig.Enabled.MouseHighlighter != isEnabled; generalSettingsConfig.Enabled.MouseHighlighter = isEnabled; break; + case "MouseJump": + needToUpdate = generalSettingsConfig.Enabled.MouseJump != isEnabled; + generalSettingsConfig.Enabled.MouseJump = isEnabled; break; case "MousePointerCrosshairs": needToUpdate = generalSettingsConfig.Enabled.MousePointerCrosshairs != isEnabled; generalSettingsConfig.Enabled.MousePointerCrosshairs = isEnabled; break; diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobeMouseUtils.xaml b/src/settings-ui/Settings.UI/OOBE/Views/OobeMouseUtils.xaml index fe19acd4bced..13c4f7637ce0 100644 --- a/src/settings-ui/Settings.UI/OOBE/Views/OobeMouseUtils.xaml +++ b/src/settings-ui/Settings.UI/OOBE/Views/OobeMouseUtils.xaml @@ -35,6 +35,13 @@ x:Uid="Oobe_MouseUtils_MousePointerCrosshairs_Description" Background="Transparent" /> + + + Draw crosshairs centered around the mouse pointer. Mouse as in the hardware peripheral. + + Mouse Jump + Mouse as in the hardware peripheral. + + + Jump the mouse pointer quickly to anywhere on your desktop. + Mouse as in the hardware peripheral. + Launch PowerToys Run @@ -3021,4 +3029,22 @@ Activate by holding the key for the character you want to add an accent to, then The maximum size, in kilobytes, for files to be displayed. This is a safety mechanism to prevent loading large files into RAM. "RAM" refers to random access memory; "size" refers to disk space; "bytes" refer to the measurement unit + + Quickly move the mouse pointer long distances. + "Mouse Jump" is the name of the utility. Mouse is the hardware mouse. + + + Mouse Jump + Refers to the utility name + + + Customize the shortcut to turn on or off this mode + + + Activation shortcut + + + Enable Mouse Jump + "Mouse Jump" is the name of the utility. + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/ViewModels/Flyout/AllAppsViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/Flyout/AllAppsViewModel.cs index 74d7a3b37c96..760e6ecba8dc 100644 --- a/src/settings-ui/Settings.UI/ViewModels/Flyout/AllAppsViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/Flyout/AllAppsViewModel.cs @@ -83,6 +83,11 @@ public AllAppsViewModel(ISettingsRepository settingsRepository, FlyoutMenuItems.Add(new FlyoutMenuItem() { Label = resourceLoader.GetString("MouseUtils_MouseHighlighter/Header"), IsEnabled = generalSettingsConfig.Enabled.MouseHighlighter, Tag = "MouseHighlighter", Icon = "ms-appx:///Assets/FluentIcons/FluentIconsMouseHighlighter.png", EnabledChangedCallback = EnabledChangedOnUI }); } + if ((gpo = GPOWrapper.GetConfiguredMouseJumpEnabledValue()) != GpoRuleConfigured.Disabled && gpo != GpoRuleConfigured.Enabled) + { + FlyoutMenuItems.Add(new FlyoutMenuItem() { Label = resourceLoader.GetString("MouseUtils_MouseJump/Header"), IsEnabled = generalSettingsConfig.Enabled.MouseJump, Tag = "MouseJump", Icon = "ms-appx:///Assets/FluentIcons/FluentIconsMouseJump.png", EnabledChangedCallback = EnabledChangedOnUI }); + } + if ((gpo = GPOWrapper.GetConfiguredMousePointerCrosshairsEnabledValue()) != GpoRuleConfigured.Disabled && gpo != GpoRuleConfigured.Enabled) { FlyoutMenuItems.Add(new FlyoutMenuItem() { Label = resourceLoader.GetString("MouseUtils_MousePointerCrosshairs/Header"), IsEnabled = generalSettingsConfig.Enabled.MousePointerCrosshairs, Tag = "MousePointerCrosshairs", Icon = "ms-appx:///Assets/FluentIcons/FluentIconsMouseCrosshairs.png", EnabledChangedCallback = EnabledChangedOnUI }); @@ -153,6 +158,7 @@ private void ModuleEnabledChangedOnSettingsPage() case "ImageResizer": item.IsEnabled = generalSettingsConfig.Enabled.ImageResizer; break; case "KeyboardManager": item.IsEnabled = generalSettingsConfig.Enabled.KeyboardManager; break; case "MouseHighlighter": item.IsEnabled = generalSettingsConfig.Enabled.MouseHighlighter; break; + case "MouseJump": item.IsEnabled = generalSettingsConfig.Enabled.MouseJump; break; case "MousePointerCrosshairs": item.IsEnabled = generalSettingsConfig.Enabled.MousePointerCrosshairs; break; case "PastePlain": item.IsEnabled = generalSettingsConfig.Enabled.PastePlain; break; case "PowerRename": item.IsEnabled = generalSettingsConfig.Enabled.PowerRename; break; diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs index a33755de8276..91c866449cac 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs @@ -21,9 +21,11 @@ public class MouseUtilsViewModel : Observable private MouseHighlighterSettings MouseHighlighterSettingsConfig { get; set; } + private MouseJumpSettings MouseJumpSettingsConfig { get; set; } + private MousePointerCrosshairsSettings MousePointerCrosshairsSettingsConfig { get; set; } - public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, ISettingsRepository findMyMouseSettingsRepository, ISettingsRepository mouseHighlighterSettingsRepository, ISettingsRepository mousePointerCrosshairsSettingsRepository, Func ipcMSGCallBackFunc) + public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, ISettingsRepository findMyMouseSettingsRepository, ISettingsRepository mouseHighlighterSettingsRepository, ISettingsRepository mouseJumpSettingsRepository, ISettingsRepository mousePointerCrosshairsSettingsRepository, Func ipcMSGCallBackFunc) { SettingsUtils = settingsUtils; @@ -78,6 +80,13 @@ public MouseUtilsViewModel(ISettingsUtils settingsUtils, ISettingsRepository _isMouseJumpEnabled; + set + { + if (_jumpEnabledStateIsGPOConfigured) + { + // If it's GPO configured, shouldn't be able to change this state. + return; + } + + if (_isMouseJumpEnabled != value) + { + _isMouseJumpEnabled = value; + + GeneralSettingsConfig.Enabled.MouseJump = value; + OnPropertyChanged(nameof(_isMouseJumpEnabled)); + + OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig); + SendConfigMSG(outgoing.ToString()); + + NotifyMouseJumpPropertyChanged(); + } + } + } + + public bool IsJumpEnabledGpoConfigured + { + get => _jumpEnabledStateIsGPOConfigured; + } + + public HotkeySettings MouseJumpActivationShortcut + { + get + { + return MouseJumpSettingsConfig.Properties.ActivationShortcut; + } + + set + { + if (MouseJumpSettingsConfig.Properties.ActivationShortcut != value) + { + MouseJumpSettingsConfig.Properties.ActivationShortcut = value; + NotifyMouseJumpPropertyChanged(); + } + } + } + + public void NotifyMouseJumpPropertyChanged([CallerMemberName] string propertyName = null) + { + OnPropertyChanged(propertyName); + + SndMouseJumpSettings outsettings = new SndMouseJumpSettings(MouseJumpSettingsConfig); + SndModuleSettings ipcMessage = new SndModuleSettings(outsettings); + SendConfigMSG(ipcMessage.ToJsonString()); + SettingsUtils.SaveSettings(MouseJumpSettingsConfig.ToJsonString(), MouseJumpSettings.ModuleName); + } + public bool IsMousePointerCrosshairsEnabled { get => _isMousePointerCrosshairsEnabled; @@ -703,6 +782,7 @@ public void RefreshEnabledState() InitializeEnabledValues(); OnPropertyChanged(nameof(IsFindMyMouseEnabled)); OnPropertyChanged(nameof(IsMouseHighlighterEnabled)); + OnPropertyChanged(nameof(IsMouseJumpEnabled)); OnPropertyChanged(nameof(IsMousePointerCrosshairsEnabled)); } @@ -732,6 +812,10 @@ public void RefreshEnabledState() private int _highlightFadeDelayMs; private int _highlightFadeDurationMs; + private GpoRuleConfigured _jumpEnabledGpoRuleConfiguration; + private bool _jumpEnabledStateIsGPOConfigured; + private bool _isMouseJumpEnabled; + private GpoRuleConfigured _mousePointerCrosshairsEnabledGpoRuleConfiguration; private bool _mousePointerCrosshairsEnabledStateIsGPOConfigured; private bool _isMousePointerCrosshairsEnabled; diff --git a/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml b/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml index 5acdf49a4dd7..09c60be62e0b 100644 --- a/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml +++ b/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml @@ -206,6 +206,31 @@ + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml.cs b/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml.cs index 149f5d49ecc3..0253b90029bd 100644 --- a/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml.cs +++ b/src/settings-ui/Settings.UI/Views/MouseUtilsPage.xaml.cs @@ -37,6 +37,7 @@ public MouseUtilsPage() SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), + SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); diff --git a/tools/BugReportTool/BugReportTool/ProcessesList.cpp b/tools/BugReportTool/BugReportTool/ProcessesList.cpp index c6e0fce3d66f..e9cae97b97f0 100644 --- a/tools/BugReportTool/BugReportTool/ProcessesList.cpp +++ b/tools/BugReportTool/BugReportTool/ProcessesList.cpp @@ -15,6 +15,7 @@ std::vector processes = L"PowerToys.PowerAccent.exe", L"PowerToys.PowerLauncher.exe", L"PowerToys.PowerOCR.exe", + L"PowerToys.MouseJumpUI.exe", L"PowerToys.MeasureToolUI.exe", L"PowerToys.ShortcutGuide.exe", L"PowerToys.PowerRename.exe", diff --git a/tools/BugReportTool/BugReportTool/ReportGPOValues.cpp b/tools/BugReportTool/BugReportTool/ReportGPOValues.cpp index dcda0253a83c..bc8a30dbc194 100644 --- a/tools/BugReportTool/BugReportTool/ReportGPOValues.cpp +++ b/tools/BugReportTool/BugReportTool/ReportGPOValues.cpp @@ -47,6 +47,7 @@ void ReportGPOValues(const std::filesystem::path& tmpDir) report << "getConfiguredKeyboardManagerEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredKeyboardManagerEnabledValue()) << std::endl; report << "getConfiguredFindMyMouseEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredFindMyMouseEnabledValue()) << std::endl; report << "getConfiguredMouseHighlighterEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMouseHighlighterEnabledValue()) << std::endl; + report << "getConfiguredMouseJumpEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMouseJumpEnabledValue()) << std::endl; report << "getConfiguredMousePointerCrosshairsEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredMousePointerCrosshairsEnabledValue()) << std::endl; report << "getConfiguredPowerRenameEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredPowerRenameEnabledValue()) << std::endl; report << "getConfiguredPowerLauncherEnabledValue: " << gpo_rule_configured_to_string(powertoys_gpo::getConfiguredPowerLauncherEnabledValue()) << std::endl;