diff --git a/.github/actions/spell-check/allow/names.txt b/.github/actions/spell-check/allow/names.txt index 76ecf535e0..ba3d9cd90a 100644 --- a/.github/actions/spell-check/allow/names.txt +++ b/.github/actions/spell-check/allow/names.txt @@ -15,6 +15,7 @@ edwinzap Essey Garside Gershaft +Guo hallatore Harmath Hemmerlein @@ -23,9 +24,11 @@ Jaswal jefflord Kamra Karthick +kevinguo Krigun Luecking Mahalingam +Mikhayelyan mshtang Myrvold naveensrinivasan @@ -33,6 +36,7 @@ nVidia Ponten Pooja robmen +robmikh Schoen skycommand snickler diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 3a7b44a779..7619c20cef 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -674,6 +674,7 @@ hcblack HCERTSTORE HCRYPTHASH HCRYPTPROV +hcursor hcwhite hdc hdrop @@ -742,7 +743,7 @@ hstring hsv htcfreek HTCLIENT -HTHUMBNAIL +hthumbnail HTOUCHINPUT HTTRANSPARENT HVal @@ -1529,6 +1530,7 @@ RECTDESTINATION RECTL rectp rects +RECTSOURCE redirectedfrom Redist redistributable @@ -1566,6 +1568,9 @@ Removelnk renamable RENAMEONCOLLISION Renamer +reparent +reparented +reparenting reparse reportbug requery diff --git a/.github/actions/spell-check/patterns.txt b/.github/actions/spell-check/patterns.txt index 5d9877ade7..f08317ddab 100644 --- a/.github/actions/spell-check/patterns.txt +++ b/.github/actions/spell-check/patterns.txt @@ -121,6 +121,7 @@ TestCase\("[^"]+" \\restart \\restore \\result +\\robmikh \\rotating \\runner \\runtimes diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index e590f9cff4..d5c6ff120b 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -33,6 +33,9 @@ "PowerToys.ColorPickerUI.dll", "PowerToys.ColorPickerUI.exe", + "PowerToys.CropAndLockModuleInterface.dll", + "PowerToys.CropAndLock.exe", + "PowerToys.PowerOCRModuleInterface.dll", "PowerToys.PowerOCR.dll", "PowerToys.PowerOCR.exe", diff --git a/COMMUNITY.md b/COMMUNITY.md index 6f831031c8..43fe021c4e 100644 --- a/COMMUNITY.md +++ b/COMMUNITY.md @@ -107,6 +107,10 @@ Randy contributed Registry Preview and some very early conversations about keybo Find My Mouse is based on Raymond Chen's SuperSonar. +### [@robmikh](https://github.com/robmikh) - Robert Mikhayelyan + +Crop And Lock is based on the original work of Robert Mikhayelyan, with Program Manager support from [@kevinguo305](https://github.com/kevinguo305) - Kevin Guo. + ### Microsoft InVEST team This amazing team helped PowerToys develop PowerToys Run and Keyboard manager as well as update our Settings to v2. @alekhyareddy28, @arjunbalgovind, @jyuwono @laviusmotileng-ms, @ryanbodrug-microsoft, @saahmedm, @somil55, @traies, @udit3333 diff --git a/PowerToys.sln b/PowerToys.sln index f636803aa1..01508db104 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -530,6 +530,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plu EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests\Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests.csproj", "{90F9FA90-2C20-4004-96E6-F3B78151F5A5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CropAndLock", "CropAndLock", "{3B227528-4BA6-4CAF-B44A-A10C78A64849}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CropAndLock", "src\modules\CropAndLock\CropAndLock\CropAndLock.vcxproj", "{F5E1146E-B7B3-4E11-85FD-270A500BD78C}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CropAndLockModuleInterface", "src\modules\CropAndLock\CropAndLockModuleInterface\CropAndLockModuleInterface.vcxproj", "{3157FA75-86CF-4EE2-8F62-C43F776493C6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2276,6 +2282,30 @@ Global {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Release|x64.Build.0 = Release|x64 {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Release|x86.ActiveCfg = Release|x64 {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Release|x86.Build.0 = Release|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|ARM64.Build.0 = Debug|ARM64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|x64.ActiveCfg = Debug|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|x64.Build.0 = Debug|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|x86.ActiveCfg = Debug|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|x86.Build.0 = Debug|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|ARM64.ActiveCfg = Release|ARM64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|ARM64.Build.0 = Release|ARM64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|x64.ActiveCfg = Release|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|x64.Build.0 = Release|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|x86.ActiveCfg = Release|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|x86.Build.0 = Release|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|ARM64.Build.0 = Debug|ARM64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|x64.ActiveCfg = Debug|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|x64.Build.0 = Debug|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|x86.ActiveCfg = Debug|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|x86.Build.0 = Debug|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|ARM64.ActiveCfg = Release|ARM64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|ARM64.Build.0 = Release|ARM64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x64.ActiveCfg = Release|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x64.Build.0 = Release|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x86.ActiveCfg = Release|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2467,6 +2497,9 @@ Global {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D} = {4AFC9975-2456-4C70-94A4-84073C1CED93} {D095BE44-1F2E-463E-A494-121892A75EA2} = {4AFC9975-2456-4C70-94A4-84073C1CED93} {90F9FA90-2C20-4004-96E6-F3B78151F5A5} = {4AFC9975-2456-4C70-94A4-84073C1CED93} + {3B227528-4BA6-4CAF-B44A-A10C78A64849} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} + {F5E1146E-B7B3-4E11-85FD-270A500BD78C} = {3B227528-4BA6-4CAF-B44A-A10C78A64849} + {3157FA75-86CF-4EE2-8F62-C43F776493C6} = {3B227528-4BA6-4CAF-B44A-A10C78A64849} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/README.md b/README.md index cab666b120..ce589d18d7 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,13 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline | | Current utilities: | | |--------------|--------------------|--------------| | [Always on Top](https://aka.ms/PowerToysOverview_AoT) | [PowerToys Awake](https://aka.ms/PowerToysOverview_Awake) | [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | -| [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) | [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | -| [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) | [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | -| [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [Peek](https://aka.ms/PowerToysOverview_Peek) | -| [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | -| [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | -| [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Video Conference Mute](https://aka.ms/PowerToysOverview_VideoConference) | +| [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) | [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | +| [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) | [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | +| [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | +| [Peek](https://aka.ms/PowerToysOverview_Peek) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | +| [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | +| [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | +| [Video Conference Mute](https://aka.ms/PowerToysOverview_VideoConference) | ## Installing and running Microsoft PowerToys diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp index 289b36feb0..579fe3ba6b 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp @@ -1005,7 +1005,7 @@ 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", @@ -1032,6 +1032,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) L"PowerToys.MouseWithoutBorders.exe", L"PowerToys.MouseWithoutBordersHelper.exe", L"PowerToys.MouseWithoutBordersService.exe", + L"PowerToys.CropAndLock.exe", L"PowerToys.exe", }; diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index cb5ac55089..46ccf722d3 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -16,6 +16,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getConfiguredColorPickerEnabledValue()); } + GpoRuleConfigured GPOWrapper::GetConfiguredCropAndLockEnabledValue() + { + return static_cast(powertoys_gpo::getConfiguredCropAndLockEnabledValue()); + } GpoRuleConfigured GPOWrapper::GetConfiguredFancyZonesEnabledValue() { return static_cast(powertoys_gpo::getConfiguredFancyZonesEnabledValue()); diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index d9e9896caf..d11feb7c3a 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -10,6 +10,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue(); static GpoRuleConfigured GetConfiguredAwakeEnabledValue(); static GpoRuleConfigured GetConfiguredColorPickerEnabledValue(); + static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue(); static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue(); static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue(); static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue(); diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index 9bec67ccf1..5e26390d7e 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -14,6 +14,7 @@ namespace PowerToys static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue(); static GpoRuleConfigured GetConfiguredAwakeEnabledValue(); static GpoRuleConfigured GetConfiguredColorPickerEnabledValue(); + static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue(); static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue(); static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue(); static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue(); diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index f5dd5dc8da..1535a3aa87 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -74,6 +74,11 @@ namespace CommonSharedConstants // Path to the event used to show Peek const wchar_t SHOW_PEEK_SHARED_EVENT[] = L"Local\\ShowPeekEvent"; + // Path to the events used by CropAndLock + const wchar_t CROP_AND_LOCK_REPARENT_EVENT[] = L"Local\\PowerToysCropAndLockReparentEvent-6060860a-76a1-44e8-8d0e-6355785e9c36"; + const wchar_t CROP_AND_LOCK_THUMBNAIL_EVENT[] = L"Local\\PowerToysCropAndLockThumbnailEvent-1637be50-da72-46b2-9220-b32b206b2434"; + const wchar_t CROP_AND_LOCK_EXIT_EVENT[] = L"Local\\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a"; + // Max DWORD for key code to disable keys. const DWORD VK_DISABLED = 0x100; } diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index eeb7274331..8498f9bf4b 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -59,7 +59,8 @@ struct LogSettings inline const static std::wstring alwaysOnTopLogPath = L"always-on-top-log.txt"; inline const static std::string hostsLoggerName = "hosts"; inline const static std::wstring hostsLogPath = L"Logs\\hosts-log.txt"; - inline const static std::string registryPreviewLoggerName = "registrypreview"; + inline const static std::string registryPreviewLoggerName = "registrypreview"; + inline const static std::string cropAndLockLoggerName = "crop-and-lock"; inline const static std::wstring registryPreviewLogPath = L"Logs\\registryPreview-log.txt"; inline const static int retention = 30; std::wstring logLevel; diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h index fa937718fb..3da87d62ae 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -22,6 +22,7 @@ namespace powertoys_gpo { const std::wstring POLICY_CONFIGURE_ENABLED_ALWAYS_ON_TOP = L"ConfigureEnabledUtilityAlwaysOnTop"; const std::wstring POLICY_CONFIGURE_ENABLED_AWAKE = L"ConfigureEnabledUtilityAwake"; const std::wstring POLICY_CONFIGURE_ENABLED_COLOR_PICKER = L"ConfigureEnabledUtilityColorPicker"; + const std::wstring POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK = L"ConfigureEnabledUtilityCropAndLock"; const std::wstring POLICY_CONFIGURE_ENABLED_FANCYZONES = L"ConfigureEnabledUtilityFancyZones"; const std::wstring POLICY_CONFIGURE_ENABLED_FILE_LOCKSMITH = L"ConfigureEnabledUtilityFileLocksmith"; const std::wstring POLICY_CONFIGURE_ENABLED_SVG_PREVIEW = L"ConfigureEnabledUtilityFileExplorerSVGPreview"; @@ -129,6 +130,11 @@ namespace powertoys_gpo { return getConfiguredValue(POLICY_CONFIGURE_ENABLED_COLOR_PICKER); } + inline gpo_rule_configured_t getConfiguredCropAndLockEnabledValue() + { + return getConfiguredValue(POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK); + } + inline gpo_rule_configured_t getConfiguredFancyZonesEnabledValue() { return getConfiguredValue(POLICY_CONFIGURE_ENABLED_FANCYZONES); diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx index 2f45a8ab29..74b319de2c 100644 --- a/src/gpo/assets/PowerToys.admx +++ b/src/gpo/assets/PowerToys.admx @@ -1,17 +1,18 @@ - + - + + @@ -51,6 +52,16 @@ + + + + + + + + + + diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml index e70495cc71..8d5e858414 100644 --- a/src/gpo/assets/en-US/PowerToys.adml +++ b/src/gpo/assets/en-US/PowerToys.adml @@ -1,7 +1,7 @@ - + PowerToys PowerToys @@ -13,6 +13,7 @@ PowerToys version 0.68.0 or later PowerToys version 0.69.0 or later PowerToys version 0.70.0 or later + PowerToys version 0.73.0 or later This policy configures the enabled state for a PowerToys utility. @@ -67,6 +68,7 @@ If this setting is disabled, experimentation is not allowed. Always On Top: Configure enabled state Awake: Configure enabled state Color Picker: Configure enabled state + Crop And Lock: Configure enabled state FancyZones: Configure enabled state File Locksmith: Configure enabled state SVG file preview: Configure enabled state diff --git a/src/modules/CropAndLock/CropAndLock/ChildWindow.cpp b/src/modules/CropAndLock/CropAndLock/ChildWindow.cpp new file mode 100644 index 0000000000..12f3e0261f --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ChildWindow.cpp @@ -0,0 +1,56 @@ +#include "pch.h" +#include "ChildWindow.h" + +namespace util +{ + using namespace robmikh::common::desktop; + using namespace robmikh::common::desktop::controls; +} + +const std::wstring ChildWindow::ClassName = L"CropAndLock.ChildWindow"; +std::once_flag ChildWindowClassRegistration; + +void ChildWindow::RegisterWindowClass() +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + WNDCLASSEXW wcex = {}; + wcex.cbSize = sizeof(wcex); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.hInstance = instance; + wcex.hIcon = LoadIconW(instance, IDI_APPLICATION); + wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wcex.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); + wcex.lpszClassName = ClassName.c_str(); + wcex.hIconSm = LoadIconW(wcex.hInstance, IDI_APPLICATION); + winrt::check_bool(RegisterClassExW(&wcex)); +} + +ChildWindow::ChildWindow(int width, int height, HWND parent) +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + + std::call_once(ChildWindowClassRegistration, []() { RegisterWindowClass(); }); + + auto exStyle = 0; + auto style = WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS; + + winrt::check_bool(CreateWindowExW(exStyle, ClassName.c_str(), L"", style, + 0, 0, width, height, parent, nullptr, instance, this)); + WINRT_ASSERT(m_window); + + ShowWindow(m_window, SW_SHOW); + UpdateWindow(m_window); +} + +LRESULT ChildWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) +{ + switch (message) + { + case WM_DESTROY: + break; + default: + return base_type::MessageHandler(message, wparam, lparam); + } + return 0; +} diff --git a/src/modules/CropAndLock/CropAndLock/ChildWindow.h b/src/modules/CropAndLock/CropAndLock/ChildWindow.h new file mode 100644 index 0000000000..3fa213676a --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ChildWindow.h @@ -0,0 +1,11 @@ +#pragma once +#include + +struct ChildWindow : robmikh::common::desktop::DesktopWindow +{ + static const std::wstring ClassName; + ChildWindow(int width, int height, HWND parent); + LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam); +private: + static void RegisterWindowClass(); +}; \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/CropAndLock.rc b/src/modules/CropAndLock/CropAndLock/CropAndLock.rc new file mode 100644 index 0000000000..a0dbc35e4e --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/CropAndLock.rc @@ -0,0 +1,105 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" +#include "../../../common/version/version.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +1 VERSIONINFO +FILEVERSION FILE_VERSION +PRODUCTVERSION PRODUCT_VERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG +FILEFLAGS VS_FF_DEBUG +#else +FILEFLAGS 0x0L +#endif +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset + BEGIN + VALUE "CompanyName", COMPANY_NAME + VALUE "FileDescription", FILE_DESCRIPTION + VALUE "FileVersion", FILE_VERSION_STRING + VALUE "InternalName", INTERNAL_NAME + VALUE "LegalCopyright", COPYRIGHT_NOTE + VALUE "OriginalFilename", ORIGINAL_FILENAME + VALUE "ProductName", PRODUCT_NAME + VALUE "ProductVersion", PRODUCT_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset + END +END + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_ICON1 ICON "icon1.ico" + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj new file mode 100644 index 0000000000..3c3e2f26f1 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj @@ -0,0 +1,170 @@ + + + + + true + true + true + true + 15.0 + {f5e1146e-b7b3-4e11-85fd-270a500bd78c} + Win32Proj + CropAndLock + 10.0.20348.0 + 10.0.19041.0 + + + + + Debug + ARM64 + + + Release + ARM64 + + + Debug + x64 + + + Release + x64 + + + + Application + v143 + v142 + v143 + v143 + Unicode + Spectre + + + true + true + + + false + true + false + + + + + + + + + + + + + + + PowerToys.$(MSBuildProjectName) + ..\..\..\..\$(Platform)\$(Configuration)\ + + + + _CONSOLE;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + Level4 + %(AdditionalOptions) /bigobj + $(SolutionDir)src\common\Telemetry;$(SolutionDir)src;%(AdditionalIncludeDirectories) + + + Windows + shell32.lib;dwmapi.lib;DbgHelp.lib;gdi32.lib;Shcore.lib;%(AdditionalDependencies) + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + MultiThreadedDebug + MultiThreadedDebug + + + false + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + MultiThreaded + MultiThreaded + + + true + true + false + + + + + + + + + + + + + + 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/CropAndLock/CropAndLock/CropAndLock.vcxproj.filters b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj.filters new file mode 100644 index 0000000000..98752f066d --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj.filters @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/CropAndLockWindow.h b/src/modules/CropAndLock/CropAndLock/CropAndLockWindow.h new file mode 100644 index 0000000000..609303e36a --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/CropAndLockWindow.h @@ -0,0 +1,10 @@ +#pragma once + +struct CropAndLockWindow +{ + virtual ~CropAndLockWindow() {} + + virtual HWND Handle() = 0; + virtual void CropAndLock(HWND windowToCrop, RECT cropRect) = 0; + virtual void OnClosed(std::function callback) = 0; +}; diff --git a/src/modules/CropAndLock/CropAndLock/DisplaysUtil.h b/src/modules/CropAndLock/CropAndLock/DisplaysUtil.h new file mode 100644 index 0000000000..b01def102b --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/DisplaysUtil.h @@ -0,0 +1,25 @@ +#pragma once + +inline RECT ComputeAllDisplaysUnion(std::vector const& infos) +{ + RECT result = {}; + result.left = LONG_MAX; + result.top = LONG_MAX; + result.right = LONG_MIN; + result.bottom = LONG_MIN; + for (auto&& info : infos) + { + auto rect = info.Rect(); + result.left = std::min(result.left, rect.left); + result.top = std::min(result.top, rect.top); + result.right = std::max(result.right, rect.right); + result.bottom = std::max(result.bottom, rect.bottom); + } + return result; +} + +inline RECT ComputeAllDisplaysUnion() +{ + auto infos = robmikh::common::desktop::DisplayInfo::GetAllDisplays(); + return ComputeAllDisplaysUnion(infos); +} diff --git a/src/modules/CropAndLock/CropAndLock/ModuleConstants.h b/src/modules/CropAndLock/CropAndLock/ModuleConstants.h new file mode 100644 index 0000000000..8cc452bb12 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ModuleConstants.h @@ -0,0 +1,6 @@ +#pragma once + +namespace NonLocalizable +{ + const inline wchar_t ModuleKey[] = L"CropAndLock"; +} \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/OverlayWindow.cpp b/src/modules/CropAndLock/CropAndLock/OverlayWindow.cpp new file mode 100644 index 0000000000..12f4598d74 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/OverlayWindow.cpp @@ -0,0 +1,356 @@ +#include "pch.h" +#include "OverlayWindow.h" + +namespace winrt +{ + using namespace Windows::UI; + using namespace Windows::UI::Composition; +} + +namespace util +{ + using namespace robmikh::common::desktop; +} + +const std::wstring OverlayWindow::ClassName = L"CropAndLock.OverlayWindow"; +const float OverlayWindow::BorderThickness = 5; +std::once_flag OverlayWindowClassRegistration; + +bool IsPointWithinRect(POINT const& point, RECT const& rect) +{ + return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom; +} + +void OverlayWindow::RegisterWindowClass() +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + WNDCLASSEXW wcex = {}; + wcex.cbSize = sizeof(wcex); + wcex.lpfnWndProc = WndProc; + wcex.hInstance = instance; + wcex.hIcon = LoadIconW(instance, IDI_APPLICATION); + wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wcex.lpszClassName = ClassName.c_str(); + wcex.hIconSm = LoadIconW(instance, IDI_APPLICATION); + winrt::check_bool(RegisterClassExW(&wcex)); +} + +OverlayWindow::OverlayWindow( + winrt::Compositor const& compositor, + HWND windowToCrop, + std::function windowCropped) +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + + std::call_once(OverlayWindowClassRegistration, []() { RegisterWindowClass(); }); + + auto exStyle = WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST; + auto style = WS_POPUP; + + // Get the union of all displays + auto displaysRect = ComputeAllDisplaysUnion(); + + // Create our window + winrt::check_bool(CreateWindowExW(exStyle, ClassName.c_str(), L"", style, + displaysRect.left, displaysRect.top, displaysRect.right - displaysRect.left, displaysRect.bottom - displaysRect.top, nullptr, nullptr, instance, this)); + WINRT_ASSERT(m_window); + + // Load cursors + m_standardCursor.reset(winrt::check_pointer(LoadCursorW(nullptr, IDC_ARROW))); + m_crosshairCursor.reset(winrt::check_pointer(LoadCursorW(nullptr, IDC_CROSS))); + m_cursorType = CursorType::Standard; + + // Setup the visual tree + m_compositor = compositor; + m_target = CreateWindowTarget(m_compositor); + m_rootVisual = m_compositor.CreateContainerVisual(); + m_shadeVisual = m_compositor.CreateSpriteVisual(); + m_windowAreaVisual = m_compositor.CreateContainerVisual(); + m_selectionVisual = m_compositor.CreateSpriteVisual(); + + m_target.Root(m_rootVisual); + auto children = m_rootVisual.Children(); + children.InsertAtBottom(m_shadeVisual); + children.InsertAtTop(m_windowAreaVisual); + m_windowAreaVisual.Children().InsertAtTop(m_selectionVisual); + + m_rootVisual.RelativeSizeAdjustment({ 1, 1 }); + m_shadeBrush = m_compositor.CreateNineGridBrush(); + m_shadeBrush.IsCenterHollow(true); + m_shadeBrush.Source(m_compositor.CreateColorBrush(winrt::Color{ 255, 0, 0, 0 })); + m_shadeVisual.Brush(m_shadeBrush); + m_shadeVisual.Opacity(0.6f); + m_shadeVisual.RelativeSizeAdjustment({ 1, 1 }); + auto selectionBrush = m_compositor.CreateNineGridBrush(); + selectionBrush.SetInsets(BorderThickness); + selectionBrush.IsCenterHollow(true); + selectionBrush.Source(m_compositor.CreateColorBrush(winrt::Color{ 255, 255, 0, 0 })); + m_selectionVisual.Brush(selectionBrush); + + WINRT_VERIFY(windowToCrop != nullptr); + m_currentWindow = windowToCrop; + SetupOverlay(); + m_windowCropped = windowCropped; + + ShowWindow(m_window, SW_SHOW); + UpdateWindow(m_window); + SetForegroundWindow(m_window); +} + +void OverlayWindow::SetupOverlay() +{ + ResetCrop(); + + // Get the client bounds of the target window + auto windowBounds = ClientAreaInScreenSpace(m_currentWindow); + + // Get the union of all displays + auto displaysRect = ComputeAllDisplaysUnion(); + + // Before we can use the window bounds, we need to + // shift the origin to the top-left most point. + m_currentWindowAreaBounds.left = windowBounds.left - displaysRect.left; + m_currentWindowAreaBounds.top = windowBounds.top - displaysRect.top; + m_currentWindowAreaBounds.right = m_currentWindowAreaBounds.left + (windowBounds.right - windowBounds.left); + m_currentWindowAreaBounds.bottom = m_currentWindowAreaBounds.top + (windowBounds.bottom - windowBounds.top); + + auto windowLeft = static_cast(m_currentWindowAreaBounds.left); + auto windowTop = static_cast(m_currentWindowAreaBounds.top); + auto windowWidth = static_cast(windowBounds.right - windowBounds.left); + auto windowHeight = static_cast(windowBounds.bottom - windowBounds.top); + + // Change the shade brush to match the window bounds + // We need to make sure the values are non-negative, as they are invalid insets. We + // can sometimes get negative values for the left and top when windows are maximized. + m_shadeBrush.LeftInset(std::max(windowLeft, 0.0f)); + m_shadeBrush.TopInset(std::max(windowTop, 0.0f)); + m_shadeBrush.RightInset(std::max(static_cast(displaysRect.right - windowBounds.right), 0.0f)); + m_shadeBrush.BottomInset(std::max(static_cast(displaysRect.bottom - windowBounds.bottom), 0.0f)); + + // Change the window area visual to match the window bounds + m_windowAreaVisual.Offset({ windowLeft, windowTop, 0 }); + m_windowAreaVisual.Size({ windowWidth, windowHeight }); + + // Reset the selection visual + m_selectionVisual.Offset({ 0, 0, 0 }); + m_selectionVisual.Size({ 0, 0 }); +} + +LRESULT OverlayWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) +{ + switch (message) + { + case WM_DESTROY: + break; + case WM_SETCURSOR: + return OnSetCursor(); + case WM_KEYUP: + { + auto key = static_cast(wparam); + if (key == VK_ESCAPE) + { + DestroyWindow(m_window); + } + } + break; + case WM_LBUTTONDOWN: + { + auto xPos = GET_X_LPARAM(lparam); + auto yPos = GET_Y_LPARAM(lparam); + OnLeftButtonDown(xPos, yPos); + } + break; + case WM_LBUTTONUP: + { + auto xPos = GET_X_LPARAM(lparam); + auto yPos = GET_Y_LPARAM(lparam); + OnLeftButtonUp(xPos, yPos); + } + break; + case WM_MOUSEMOVE: + { + auto xPos = GET_X_LPARAM(lparam); + auto yPos = GET_Y_LPARAM(lparam); + OnMouseMove(xPos, yPos); + } + break; + default: + return base_type::MessageHandler(message, wparam, lparam); + } + return 0; +} + +void OverlayWindow::ResetCrop() +{ + m_cropStatus = CropStatus::None; + m_startPosition = {}; + m_cropRect = {}; +} + +bool OverlayWindow::OnSetCursor() +{ + switch (m_cursorType) + { + case CursorType::Standard: + SetCursor(m_standardCursor.get()); + return true; + case CursorType::Crosshair: + SetCursor(m_crosshairCursor.get()); + return true; + default: + return false; + } +} + +void OverlayWindow::OnLeftButtonDown(int x, int y) +{ + if (m_cropStatus == CropStatus::None) + { + if (!IsPointWithinRect({ x, y }, m_currentWindowAreaBounds)) + { + DestroyWindow(m_window); + return; + } + + m_cropStatus = CropStatus::Ongoing; + + x -= m_currentWindowAreaBounds.left; + y -= m_currentWindowAreaBounds.top; + + m_selectionVisual.Offset({ x - BorderThickness, y - BorderThickness, 0 }); + m_startPosition = { x, y }; + } +} + +void OverlayWindow::OnLeftButtonUp(int x, int y) +{ + if (m_cropStatus == CropStatus::Ongoing) + { + m_cropStatus = CropStatus::Completed; + m_cursorType = CursorType::Standard; + + // For debugging, it's easier if the window doesn't block the screen after this point + ShowWindow(m_window, SW_HIDE); + + if (x < m_currentWindowAreaBounds.left) + { + x = m_currentWindowAreaBounds.left; + } + else if (x > m_currentWindowAreaBounds.right) + { + x = m_currentWindowAreaBounds.right; + } + + if (y < m_currentWindowAreaBounds.top) + { + y = m_currentWindowAreaBounds.top; + } + else if (y > m_currentWindowAreaBounds.bottom) + { + y = m_currentWindowAreaBounds.bottom; + } + + x -= m_currentWindowAreaBounds.left; + y -= m_currentWindowAreaBounds.top; + + // Compute our crop rect + if (x < m_startPosition.x) + { + m_cropRect.left = x; + m_cropRect.right = m_startPosition.x; + } + else + { + m_cropRect.left = m_startPosition.x; + m_cropRect.right = x; + } + if (y < m_startPosition.y) + { + m_cropRect.top = y; + m_cropRect.bottom = m_startPosition.y; + } + else + { + m_cropRect.top = m_startPosition.y; + m_cropRect.bottom = y; + } + + // Exit if the rect is empty + if (m_cropRect.right - m_cropRect.left == 0 || m_cropRect.bottom - m_cropRect.top == 0) + { + DestroyWindow(m_window); + return; + } + + // Fire the callback + if (m_windowCropped != nullptr) + { + m_windowCropped(m_currentWindow, m_cropRect); + } + DestroyWindow(m_window); + } +} + +void OverlayWindow::OnMouseMove(int x, int y) +{ + if (m_cropStatus == CropStatus::None) + { + if (IsPointWithinRect({ x, y }, m_currentWindowAreaBounds)) + { + m_cursorType = CursorType::Crosshair; + } + else + { + m_cursorType = CursorType::Standard; + } + } + else if (m_cropStatus == CropStatus::Ongoing) + { + if (x < m_currentWindowAreaBounds.left) + { + x = m_currentWindowAreaBounds.left; + } + else if (x > m_currentWindowAreaBounds.right) + { + x = m_currentWindowAreaBounds.right; + } + + if (y < m_currentWindowAreaBounds.top) + { + y = m_currentWindowAreaBounds.top; + } + else if (y > m_currentWindowAreaBounds.bottom) + { + y = m_currentWindowAreaBounds.bottom; + } + + x -= m_currentWindowAreaBounds.left; + y -= m_currentWindowAreaBounds.top; + + auto offset = m_selectionVisual.Offset(); + auto size = m_selectionVisual.Size(); + + if (x < m_startPosition.x) + { + offset.x = x - BorderThickness; + size.x = (m_startPosition.x - x) + (2 * BorderThickness); + } + else + { + size.x = (x - m_startPosition.x) + (2 * BorderThickness); + } + + if (y < m_startPosition.y) + { + offset.y = y - BorderThickness; + size.y = (m_startPosition.y - y) + (2 * BorderThickness); + } + else + { + size.y = (y - m_startPosition.y) + (2 * BorderThickness); + } + + m_selectionVisual.Offset(offset); + m_selectionVisual.Size(size); + } +} diff --git a/src/modules/CropAndLock/CropAndLock/OverlayWindow.h b/src/modules/CropAndLock/CropAndLock/OverlayWindow.h new file mode 100644 index 0000000000..67d59c56a4 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/OverlayWindow.h @@ -0,0 +1,58 @@ +#pragma once +#include + +struct OverlayWindow : robmikh::common::desktop::DesktopWindow +{ + static const std::wstring ClassName; + OverlayWindow( + winrt::Windows::UI::Composition::Compositor const& compositor, + HWND windowToCrop, + std::function windowCropped); + ~OverlayWindow() { m_windowCropped = nullptr; DestroyWindow(m_window); } + LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam); + +private: + enum class CursorType + { + Standard, + Crosshair, + }; + + enum class CropStatus + { + None, + Ongoing, + Completed, + }; + + static const float BorderThickness; + static void RegisterWindowClass(); + + void SetupOverlay(); + void ResetCrop(); + bool OnSetCursor(); + void OnLeftButtonDown(int x, int y); + void OnLeftButtonUp(int x, int y); + void OnMouseMove(int x, int y); + +private: + std::function m_windowCropped; + winrt::Windows::UI::Composition::Compositor m_compositor{ nullptr }; + winrt::Windows::UI::Composition::CompositionTarget m_target{ nullptr }; + winrt::Windows::UI::Composition::ContainerVisual m_rootVisual{ nullptr }; + winrt::Windows::UI::Composition::SpriteVisual m_shadeVisual{ nullptr }; + winrt::Windows::UI::Composition::ContainerVisual m_windowAreaVisual{ nullptr }; + winrt::Windows::UI::Composition::SpriteVisual m_selectionVisual{ nullptr }; + winrt::Windows::UI::Composition::CompositionNineGridBrush m_shadeBrush{ nullptr }; + + HWND m_currentWindow = nullptr; + RECT m_currentWindowAreaBounds = {}; + + CropStatus m_cropStatus = CropStatus::None; + POINT m_startPosition = {}; + RECT m_cropRect = {}; + + CursorType m_cursorType = CursorType::Standard; + wil::unique_hcursor m_standardCursor; + wil::unique_hcursor m_crosshairCursor; +}; diff --git a/src/modules/CropAndLock/CropAndLock/PropertySheet.props b/src/modules/CropAndLock/CropAndLock/PropertySheet.props new file mode 100644 index 0000000000..b0c622690f --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/PropertySheet.props @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.cpp b/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.cpp new file mode 100644 index 0000000000..cdf9fb3eb0 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.cpp @@ -0,0 +1,160 @@ +#include "pch.h" +#include "ReparentCropAndLockWindow.h" + +const std::wstring ReparentCropAndLockWindow::ClassName = L"CropAndLock.ReparentCropAndLockWindow"; +std::once_flag ReparentCropAndLockWindowClassRegistration; + +void ReparentCropAndLockWindow::RegisterWindowClass() +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + WNDCLASSEXW wcex = {}; + wcex.cbSize = sizeof(wcex); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.hInstance = instance; + wcex.hIcon = LoadIconW(instance, IDI_APPLICATION); + wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wcex.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); + wcex.lpszClassName = ClassName.c_str(); + wcex.hIconSm = LoadIconW(wcex.hInstance, IDI_APPLICATION); + winrt::check_bool(RegisterClassExW(&wcex)); +} + +ReparentCropAndLockWindow::ReparentCropAndLockWindow(std::wstring const& titleString, int width, int height) +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + + std::call_once(ReparentCropAndLockWindowClassRegistration, []() { RegisterWindowClass(); }); + + auto exStyle = 0; + auto style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN; + style &= ~(WS_MAXIMIZEBOX | WS_THICKFRAME); + + RECT rect = { 0, 0, width, height}; + winrt::check_bool(AdjustWindowRectEx(&rect, style, false, exStyle)); + auto adjustedWidth = rect.right - rect.left; + auto adjustedHeight = rect.bottom - rect.top; + + winrt::check_bool(CreateWindowExW(exStyle, ClassName.c_str(), titleString.c_str(), style, + CW_USEDEFAULT, CW_USEDEFAULT, adjustedWidth, adjustedHeight, nullptr, nullptr, instance, this)); + WINRT_ASSERT(m_window); + + m_childWindow = std::make_unique(width, height, m_window); +} + +ReparentCropAndLockWindow::~ReparentCropAndLockWindow() +{ + DisconnectTarget(); + DestroyWindow(m_window); +} + +LRESULT ReparentCropAndLockWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) +{ + switch (message) + { + case WM_DESTROY: + if (m_closedCallback != nullptr && !m_destroyed) + { + m_destroyed = true; + m_closedCallback(m_window); + } + break; + case WM_MOUSEACTIVATE: + if (m_currentTarget != nullptr && GetForegroundWindow() != m_currentTarget) + { + SetForegroundWindow(m_currentTarget); + } + return MA_NOACTIVATE; + case WM_ACTIVATE: + if (static_cast(wparam) == WA_ACTIVE) + { + if (m_currentTarget != nullptr) + { + SetForegroundWindow(m_currentTarget); + } + } + break; + case WM_DPICHANGED: + break; + default: + return base_type::MessageHandler(message, wparam, lparam); + } + return 0; +} + +void ReparentCropAndLockWindow::CropAndLock(HWND windowToCrop, RECT cropRect) +{ + DisconnectTarget(); + m_currentTarget = windowToCrop; + + // Adjust the crop rect to be in the window space as reported by win32k + RECT windowRect = {}; + winrt::check_bool(GetWindowRect(m_currentTarget, &windowRect)); + auto clientRect = ClientAreaInScreenSpace(m_currentTarget); + auto diffX = clientRect.left - windowRect.left; + auto diffY = clientRect.top - windowRect.top; + auto adjustedCropRect = cropRect; + adjustedCropRect.left += diffX; + adjustedCropRect.top += diffY; + adjustedCropRect.right += diffX; + adjustedCropRect.bottom += diffY; + cropRect = adjustedCropRect; + + // Save the previous position of the target so that we can restore it. + m_previousPosition = { windowRect.left, windowRect.top }; + auto newX = adjustedCropRect.left + windowRect.left; + auto newY = adjustedCropRect.top + windowRect.top; + + auto monitor = winrt::check_pointer(MonitorFromWindow(m_currentTarget, MONITOR_DEFAULTTONULL)); + uint32_t dpiX = 0; + uint32_t dpiY = 0; + winrt::check_hresult(GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY)); + uint32_t dpi = dpiX > dpiY ? dpiX : dpiY; + + // Reconfigure our window + auto width = cropRect.right - cropRect.left; + auto height = cropRect.bottom - cropRect.top; + windowRect = { newX, newY, newX + width, newY + height }; + auto exStyle = static_cast(GetWindowLongPtrW(m_window, GWL_EXSTYLE)); + auto style = static_cast(GetWindowLongPtrW(m_window, GWL_STYLE)); + winrt::check_bool(AdjustWindowRectExForDpi(&windowRect, style, false, exStyle, dpi)); + auto adjustedWidth = windowRect.right - windowRect.left; + auto adjustedHeight = windowRect.bottom - windowRect.top; + + winrt::check_bool(SetWindowPos(m_window, HWND_TOPMOST, windowRect.left, windowRect.top, adjustedWidth, adjustedHeight, SWP_SHOWWINDOW | SWP_NOACTIVATE)); + winrt::check_bool(SetWindowPos(m_childWindow->m_window, nullptr, 0, 0, width, height, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE)); + + // Reparent the target window + SetParent(m_currentTarget, m_childWindow->m_window); + auto targetStyle = GetWindowLongPtrW(m_currentTarget, GWL_STYLE); + targetStyle |= WS_CHILD; + SetWindowLongPtrW(m_currentTarget, GWL_STYLE, targetStyle); + auto x = -cropRect.left; + auto y = -cropRect.top; + winrt::check_bool(SetWindowPos(m_currentTarget, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOZORDER)); +} + +void ReparentCropAndLockWindow::Hide() +{ + DisconnectTarget(); + ShowWindow(m_window, SW_HIDE); +} + +void ReparentCropAndLockWindow::DisconnectTarget() +{ + if (m_currentTarget != nullptr) + { + if (!IsWindow(m_currentTarget)) + { + // The child window was closed by other means? + m_currentTarget = nullptr; + return; + } + winrt::check_bool(SetWindowPos(m_currentTarget, nullptr, m_previousPosition.x, m_previousPosition.y, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED)); + SetParent(m_currentTarget, nullptr); + auto targetStyle = static_cast(GetWindowLongPtrW(m_currentTarget, GWL_STYLE)); + targetStyle &= ~WS_CHILD; + SetWindowLongPtrW(m_currentTarget, GWL_STYLE, targetStyle); + m_currentTarget = nullptr; + } +} diff --git a/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.h b/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.h new file mode 100644 index 0000000000..4734340e34 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include "CropAndLockWindow.h" +#include "ChildWindow.h" + +struct ReparentCropAndLockWindow : robmikh::common::desktop::DesktopWindow, CropAndLockWindow +{ + static const std::wstring ClassName; + ReparentCropAndLockWindow(std::wstring const& titleString, int width, int height); + ~ReparentCropAndLockWindow() override; + LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam); + + HWND Handle() override { return m_window; } + void CropAndLock(HWND windowToCrop, RECT cropRect) override; + void OnClosed(std::function callback) override { m_closedCallback = callback; } + +private: + static void RegisterWindowClass(); + + void Hide(); + void DisconnectTarget(); + +private: + HWND m_currentTarget = nullptr; + POINT m_previousPosition = {}; + std::unique_ptr m_childWindow; + bool m_destroyed = false; + std::function m_closedCallback; +}; \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/SettingsWindow.h b/src/modules/CropAndLock/CropAndLock/SettingsWindow.h new file mode 100644 index 0000000000..88489601ee --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/SettingsWindow.h @@ -0,0 +1,7 @@ +#pragma once + +enum class CropAndLockType +{ + Reparent, + Thumbnail, +}; diff --git a/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.cpp b/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.cpp new file mode 100644 index 0000000000..454e8d5abe --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.cpp @@ -0,0 +1,181 @@ +#include "pch.h" +#include "ThumbnailCropAndLockWindow.h" + +const std::wstring ThumbnailCropAndLockWindow::ClassName = L"CropAndLock.ThumbnailCropAndLockWindow"; +std::once_flag ThumbnailCropAndLockWindowClassRegistration; + +float ComputeScaleFactor(RECT const& windowRect, RECT const& contentRect) +{ + auto windowWidth = static_cast(windowRect.right - windowRect.left); + auto windowHeight = static_cast(windowRect.bottom - windowRect.top); + auto contentWidth = static_cast(contentRect.right - contentRect.left); + auto contentHeight = static_cast(contentRect.bottom - contentRect.top); + + auto windowRatio = windowWidth / windowHeight; + auto contentRatio = contentWidth / contentHeight; + + auto scaleFactor = windowWidth / contentWidth; + if (windowRatio > contentRatio) + { + scaleFactor = windowHeight / contentHeight; + } + + return scaleFactor; +} + +RECT ComputeDestRect(RECT const& windowRect, RECT const& contentRect) +{ + auto scaleFactor = ComputeScaleFactor(windowRect, contentRect); + + auto windowWidth = static_cast(windowRect.right - windowRect.left); + auto windowHeight = static_cast(windowRect.bottom - windowRect.top); + auto contentWidth = static_cast(contentRect.right - contentRect.left) * scaleFactor; + auto contentHeight = static_cast(contentRect.bottom - contentRect.top) * scaleFactor; + + auto remainingWidth = windowWidth - contentWidth; + auto remainingHeight = windowHeight - contentHeight; + + auto left = static_cast(remainingWidth / 2.0f); + auto top = static_cast(remainingHeight / 2.0f); + auto right = left + static_cast(contentWidth); + auto bottom = top + static_cast(contentHeight); + + return RECT{ left, top, right, bottom }; +} + +void ThumbnailCropAndLockWindow::RegisterWindowClass() +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + WNDCLASSEXW wcex = {}; + wcex.cbSize = sizeof(wcex); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.hInstance = instance; + wcex.hIcon = LoadIconW(instance, IDI_APPLICATION); + wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wcex.hbrBackground = static_cast(GetStockObject(BLACK_BRUSH)); + wcex.lpszClassName = ClassName.c_str(); + wcex.hIconSm = LoadIconW(wcex.hInstance, IDI_APPLICATION); + winrt::check_bool(RegisterClassExW(&wcex)); +} + +ThumbnailCropAndLockWindow::ThumbnailCropAndLockWindow(std::wstring const& titleString, int width, int height) +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + + std::call_once(ThumbnailCropAndLockWindowClassRegistration, []() { RegisterWindowClass(); }); + + auto exStyle = 0; + auto style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN; + + RECT rect = { 0, 0, width, height}; + winrt::check_bool(AdjustWindowRectEx(&rect, style, false, exStyle)); + auto adjustedWidth = rect.right - rect.left; + auto adjustedHeight = rect.bottom - rect.top; + + winrt::check_bool(CreateWindowExW(exStyle, ClassName.c_str(), titleString.c_str(), style, + CW_USEDEFAULT, CW_USEDEFAULT, adjustedWidth, adjustedHeight, nullptr, nullptr, instance, this)); + WINRT_ASSERT(m_window); +} + +ThumbnailCropAndLockWindow::~ThumbnailCropAndLockWindow() +{ + DisconnectTarget(); + DestroyWindow(m_window); +} + +LRESULT ThumbnailCropAndLockWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) +{ + switch (message) + { + case WM_DESTROY: + if (m_closedCallback != nullptr && !m_destroyed) + { + m_destroyed = true; + m_closedCallback(m_window); + } + break; + case WM_SIZE: + case WM_SIZING: + { + if (m_thumbnail != nullptr) + { + RECT clientRect = {}; + winrt::check_bool(GetClientRect(m_window, &clientRect)); + + m_destRect = ComputeDestRect(clientRect, m_sourceRect); + + DWM_THUMBNAIL_PROPERTIES properties = {}; + properties.dwFlags = DWM_TNP_RECTDESTINATION; + properties.rcDestination = m_destRect; + winrt::check_hresult(DwmUpdateThumbnailProperties(m_thumbnail.get(), &properties)); + } + } + break; + default: + return base_type::MessageHandler(message, wparam, lparam); + } + return 0; +} + +void ThumbnailCropAndLockWindow::CropAndLock(HWND windowToCrop, RECT cropRect) +{ + DisconnectTarget(); + m_currentTarget = windowToCrop; + + // Adjust the crop rect to be in the window space as reported by the DWM + RECT windowRect = {}; + winrt::check_hresult(DwmGetWindowAttribute(m_currentTarget, DWMWA_EXTENDED_FRAME_BOUNDS, reinterpret_cast(&windowRect), sizeof(windowRect))); + auto clientRect = ClientAreaInScreenSpace(m_currentTarget); + auto diffX = clientRect.left - windowRect.left; + auto diffY = clientRect.top - windowRect.top; + auto adjustedCropRect = cropRect; + adjustedCropRect.left += diffX; + adjustedCropRect.top += diffY; + adjustedCropRect.right += diffX; + adjustedCropRect.bottom += diffY; + cropRect = adjustedCropRect; + + // Resize our window + auto width = cropRect.right - cropRect.left; + auto height = cropRect.bottom - cropRect.top; + windowRect = { 0, 0, width, height }; + auto exStyle = static_cast(GetWindowLongPtrW(m_window, GWL_EXSTYLE)); + auto style = static_cast(GetWindowLongPtrW(m_window, GWL_STYLE)); + winrt::check_bool(AdjustWindowRectEx(&windowRect, style, false, exStyle)); + auto adjustedWidth = windowRect.right - windowRect.left; + auto adjustedHeight = windowRect.bottom - windowRect.top; + winrt::check_bool(SetWindowPos(m_window, HWND_TOPMOST, 0, 0, adjustedWidth, adjustedHeight, SWP_NOMOVE | SWP_SHOWWINDOW)); + + // Setup the thumbnail + winrt::check_hresult(DwmRegisterThumbnail(m_window, m_currentTarget, m_thumbnail.addressof())); + + clientRect = {}; + winrt::check_bool(GetClientRect(m_window, &clientRect)); + m_destRect = clientRect; + m_sourceRect = cropRect; + + DWM_THUMBNAIL_PROPERTIES properties = {}; + properties.dwFlags = DWM_TNP_SOURCECLIENTAREAONLY | DWM_TNP_VISIBLE | DWM_TNP_OPACITY | DWM_TNP_RECTDESTINATION | DWM_TNP_RECTSOURCE; + properties.fSourceClientAreaOnly = false; + properties.fVisible = true; + properties.opacity = 255; + properties.rcDestination = m_destRect; + properties.rcSource = m_sourceRect; + winrt::check_hresult(DwmUpdateThumbnailProperties(m_thumbnail.get(), &properties)); +} + +void ThumbnailCropAndLockWindow::Hide() +{ + DisconnectTarget(); + ShowWindow(m_window, SW_HIDE); +} + +void ThumbnailCropAndLockWindow::DisconnectTarget() +{ + if (m_currentTarget != nullptr) + { + m_thumbnail.reset(); + m_currentTarget = nullptr; + } +} diff --git a/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.h b/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.h new file mode 100644 index 0000000000..c75fe8c94a --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include "CropAndLockWindow.h" + +struct ThumbnailCropAndLockWindow : robmikh::common::desktop::DesktopWindow, CropAndLockWindow +{ + static const std::wstring ClassName; + ThumbnailCropAndLockWindow(std::wstring const& titleString, int width, int height); + ~ThumbnailCropAndLockWindow() override; + LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam); + + HWND Handle() override { return m_window; } + void CropAndLock(HWND windowToCrop, RECT cropRect) override; + void OnClosed(std::function callback) override { m_closedCallback = callback; } + +private: + static void RegisterWindowClass(); + + void Hide(); + void DisconnectTarget(); + +private: + HWND m_currentTarget = nullptr; + POINT m_previousPosition = {}; + + unique_hthumbnail m_thumbnail; + RECT m_destRect = {}; + RECT m_sourceRect = {}; + + bool m_destroyed = false; + std::function m_closedCallback; +}; \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/ThumbnailUtil.h b/src/modules/CropAndLock/CropAndLock/ThumbnailUtil.h new file mode 100644 index 0000000000..d919869a35 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ThumbnailUtil.h @@ -0,0 +1,3 @@ +#pragma once + +typedef wil::unique_any unique_hthumbnail; \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/WindowRectUtil.h b/src/modules/CropAndLock/CropAndLock/WindowRectUtil.h new file mode 100644 index 0000000000..8b59fdac32 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/WindowRectUtil.h @@ -0,0 +1,14 @@ +#pragma once + +inline RECT ClientAreaInScreenSpace(HWND window) +{ + POINT clientOrigin = { 0, 0 }; + winrt::check_bool(ClientToScreen(window, &clientOrigin)); + RECT windowBounds = {}; + winrt::check_bool(GetClientRect(window, &windowBounds)); + windowBounds.left += clientOrigin.x; + windowBounds.top += clientOrigin.y; + windowBounds.right += clientOrigin.x; + windowBounds.bottom += clientOrigin.y; + return windowBounds; +} diff --git a/src/modules/CropAndLock/CropAndLock/app.manifest b/src/modules/CropAndLock/CropAndLock/app.manifest new file mode 100644 index 0000000000..92f5a62639 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/app.manifest @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/modules/CropAndLock/CropAndLock/icon1.ico b/src/modules/CropAndLock/CropAndLock/icon1.ico new file mode 100644 index 0000000000..5d06b9f285 Binary files /dev/null and b/src/modules/CropAndLock/CropAndLock/icon1.ico differ diff --git a/src/modules/CropAndLock/CropAndLock/main.cpp b/src/modules/CropAndLock/CropAndLock/main.cpp new file mode 100644 index 0000000000..5945cd63ed --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/main.cpp @@ -0,0 +1,270 @@ +#include "pch.h" +#include "SettingsWindow.h" +#include "OverlayWindow.h" +#include "CropAndLockWindow.h" +#include "ThumbnailCropAndLockWindow.h" +#include "ReparentCropAndLockWindow.h" +#include +#include +#include +#include +#include +#include "ModuleConstants.h" +#include +#include "trace.h" + +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") + +namespace winrt +{ + using namespace Windows::Foundation; + using namespace Windows::Foundation::Numerics; + using namespace Windows::UI; + using namespace Windows::UI::Composition; +} + +namespace util +{ + using namespace robmikh::common::desktop; +} + +const std::wstring instanceMutexName = L"Local\\PowerToys_CropAndLock_InstanceMutex"; +bool m_running = true; + +int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _In_ int) +{ + // Initialize COM + winrt::init_apartment(winrt::apartment_type::single_threaded); + + // Initialize logger automatic logging of exceptions. + LoggerHelpers::init_logger(NonLocalizable::ModuleKey, L"", LogSettings::cropAndLockLoggerName); + InitUnhandledExceptionHandler(); + + if (powertoys_gpo::getConfiguredCropAndLockEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) + { + Logger::warn(L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator."); + return 0; + } + + // Before we do anything, check to see if we're already running. If we are, + // the hotkey won't register and we'll fail. Instead, we should tell the user + // to kill the other instance and exit this one. + auto mutex = CreateMutex(nullptr, true, instanceMutexName.c_str()); + if (mutex == nullptr) + { + Logger::error(L"Failed to create mutex. {}", get_last_error_or_default(GetLastError())); + } + + if (GetLastError() == ERROR_ALREADY_EXISTS) + { + // CropAndLock is already open. + return 1; + } + + std::wstring pid = std::wstring(lpCmdLine); + if (pid.empty()) + { + Logger::warn(L"Tried to run Crop And Lock as a standalone."); + MessageBoxW(nullptr, L"CropAndLock can't run as a standalone. Start it from PowerToys.", L"CropAndLock", MB_ICONERROR); + return 1; + } + + auto mainThreadId = GetCurrentThreadId(); + ProcessWaiter::OnProcessTerminate(pid, [mainThreadId](int err) { + if (err != ERROR_SUCCESS) + { + Logger::error(L"Failed to wait for parent process exit. {}", get_last_error_or_default(err)); + } + else + { + Logger::trace(L"PowerToys runner exited."); + } + + Logger::trace(L"Exiting CropAndLock"); + PostThreadMessage(mainThreadId, WM_QUIT, 0, 0); + }); + + // NOTE: reparenting a window with a different DPI context has consequences. + // See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setparent#remarks + // for more info. + winrt::check_bool(SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)); + + // Create the DispatcherQueue that the compositor needs to run + auto controller = util::CreateDispatcherQueueControllerForCurrentThread(); + + // Setup Composition + auto compositor = winrt::Compositor(); + + // Create our overlay window + std::unique_ptr overlayWindow; + + // Keep a list of our cropped windows + std::vector> croppedWindows; + + // Handles and thread for the events sent from runner + HANDLE m_reparent_event_handle; + HANDLE m_thumbnail_event_handle; + HANDLE m_exit_event_handle; + std::thread m_event_triggers_thread; + + std::function removeWindowCallback = [&](HWND windowHandle) + { + if (!m_running) + { + // If we're not running, the reference to croppedWindows might no longer be valid and cause a crash at exit time, due to being called by destructors after wWinMain returns. + return; + } + + auto pos = std::find_if(croppedWindows.begin(), croppedWindows.end(), [windowHandle](auto window) { return window->Handle() == windowHandle; }); + if (pos != croppedWindows.end()) + { + croppedWindows.erase(pos); + } + }; + + std::function ProcessCommand = [&](CropAndLockType mode) + { + std::function windowCroppedCallback = [&, mode](HWND targetWindow, RECT cropRect) { + auto targetInfo = util::WindowInfo(targetWindow); + // TODO: Fix WindowInfo.h to not contain the null char at the end. + auto nullCharIndex = std::wstring::npos; + do + { + nullCharIndex = targetInfo.Title.rfind(L'\0'); + if (nullCharIndex != std::wstring::npos) + { + targetInfo.Title.erase(nullCharIndex); + } + } while (nullCharIndex != std::wstring::npos); + + std::wstringstream titleStream; + titleStream << targetInfo.Title << L" (Cropped)"; + auto title = titleStream.str(); + + std::shared_ptr croppedWindow; + switch (mode) + { + case CropAndLockType::Reparent: + croppedWindow = std::make_shared(title, 800, 600); + Logger::trace(L"Creating a reparent window"); + Trace::CropAndLock::CreateReparentWindow(); + break; + case CropAndLockType::Thumbnail: + croppedWindow = std::make_shared(title, 800, 600); + Logger::trace(L"Creating a thumbnail window"); + Trace::CropAndLock::CreateThumbnailWindow(); + break; + default: + return; + } + croppedWindow->CropAndLock(targetWindow, cropRect); + croppedWindow->OnClosed(removeWindowCallback); + croppedWindows.push_back(croppedWindow); + }; + + overlayWindow.reset(); + + // Get the current window with focus + auto foregroundWindow = GetForegroundWindow(); + if (foregroundWindow != nullptr) + { + bool match = false; + for (auto&& croppedWindow : croppedWindows) + { + if (foregroundWindow == croppedWindow->Handle()) + { + match = true; + break; + } + } + if (!match) + { + overlayWindow = std::make_unique(compositor, foregroundWindow, windowCroppedCallback); + } + } + }; + + // Start a thread to listen on the events. + m_reparent_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT); + m_thumbnail_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_THUMBNAIL_EVENT); + m_exit_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_EXIT_EVENT); + if (!m_reparent_event_handle || !m_reparent_event_handle || !m_exit_event_handle) + { + Logger::warn(L"Failed to create events. {}", get_last_error_or_default(GetLastError())); + return 1; + } + + m_event_triggers_thread = std::thread([&]() { + MSG msg; + HANDLE event_handles[3] = {m_reparent_event_handle, m_thumbnail_event_handle, m_exit_event_handle}; + while (m_running) + { + DWORD dwEvt = MsgWaitForMultipleObjects(3, event_handles, false, INFINITE, QS_ALLINPUT); + if (!m_running) + { + break; + } + switch (dwEvt) + { + case WAIT_OBJECT_0: + { + // Reparent Event + bool enqueueSucceeded = controller.DispatcherQueue().TryEnqueue([&]() { + ProcessCommand(CropAndLockType::Reparent); + }); + if (!enqueueSucceeded) + { + Logger::error("Couldn't enqueue message to reparent a window."); + } + break; + } + case WAIT_OBJECT_0 + 1: + { + // Thumbnail Event + bool enqueueSucceeded = controller.DispatcherQueue().TryEnqueue([&]() { + ProcessCommand(CropAndLockType::Thumbnail); + }); + if (!enqueueSucceeded) + { + Logger::error("Couldn't enqueue message to thumbnail a window."); + } + break; + } + case WAIT_OBJECT_0 + 2: + { + // Exit Event + Logger::trace(L"Received an exit event."); + PostThreadMessage(mainThreadId, WM_QUIT, 0, 0); + break; + } + case WAIT_OBJECT_0 + 3: + if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + break; + default: + break; + } + } + }); + + // Message pump + MSG msg = {}; + while (GetMessageW(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + + m_running = false; + // Needed to unblock MsgWaitForMultipleObjects one last time + SetEvent(m_reparent_event_handle); + CloseHandle(m_reparent_event_handle); + CloseHandle(m_thumbnail_event_handle); + CloseHandle(m_exit_event_handle); + m_event_triggers_thread.join(); + + return util::ShutdownDispatcherQueueControllerAndWait(controller, static_cast(msg.wParam)); +} diff --git a/src/modules/CropAndLock/CropAndLock/packages.config b/src/modules/CropAndLock/CropAndLock/packages.config new file mode 100644 index 0000000000..abf15d0beb --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/pch.cpp b/src/modules/CropAndLock/CropAndLock/pch.cpp new file mode 100644 index 0000000000..bcb5590be1 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/CropAndLock/CropAndLock/pch.h b/src/modules/CropAndLock/CropAndLock/pch.h new file mode 100644 index 0000000000..fe9e5e635e --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/pch.h @@ -0,0 +1,78 @@ +#pragma once + +// Collision from minWinDef min/max and std +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN + +// Windows +#include +#include + +// Must come before C++/WinRT +#include + +// WinRT +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// WIL +#include + +// DirectX +#include +#include +#include +#include + +// DWM +#include + +// Shell +#include + +// STL +#include +#include +#include +#include +#include +#include +#include + +// robmikh.common +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Helpers +#include "DisplaysUtil.h" +#include "ThumbnailUtil.h" +#include "WindowRectUtil.h" + +// PowerToys +#include +#include + +// Application resources +#include "resource.h" +#define MAIN_ICON MAKEINTRESOURCEW(IDI_ICON1) \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/resource.h b/src/modules/CropAndLock/CropAndLock/resource.h new file mode 100644 index 0000000000..19dd3a8e12 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/resource.h @@ -0,0 +1,26 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by CropAndLock.rc +// +#define IDI_ICON1 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys CropAndLock" +#define INTERNAL_NAME "PowerToys.CropAndLock" +#define ORIGINAL_FILENAME "PowerToys.CropAndLock.exe" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/CropAndLock/CropAndLock/trace.cpp b/src/modules/CropAndLock/CropAndLock/trace.cpp new file mode 100644 index 0000000000..fb5dd802c5 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/trace.cpp @@ -0,0 +1,95 @@ +#include "pch.h" +#include "trace.h" + +// Telemetry strings should not be localized. +#define LoggingProviderKey "Microsoft.PowerToys" + +TRACELOGGING_DEFINE_PROVIDER( + g_hProvider, + LoggingProviderKey, + // {38e8889b-9731-53f5-e901-e8a7c1753074} + (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74), + TraceLoggingOptionProjectTelemetry()); + +void Trace::RegisterProvider() noexcept +{ + TraceLoggingRegister(g_hProvider); +} + +void Trace::UnregisterProvider() noexcept +{ + TraceLoggingUnregister(g_hProvider); +} + +void Trace::CropAndLock::Enable(bool enabled) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "CropAndLock_EnableCropAndLock", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, "Enabled")); +} + +void Trace::CropAndLock::ActivateReparent() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "CropAndLock_ActivateReparent", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + +void Trace::CropAndLock::ActivateThumbnail() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "CropAndLock_ActivateThumbnail", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + +void Trace::CropAndLock::CreateReparentWindow() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "CropAndLock_CreateReparentWindow", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + +void Trace::CropAndLock::CreateThumbnailWindow() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "CropAndLock_CreateThumbnailWindow", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + +// Event to send settings telemetry. +void Trace::CropAndLock::SettingsTelemetry(PowertoyModuleIface::Hotkey& reparentHotkey, PowertoyModuleIface::Hotkey& thumbnailHotkey) noexcept +{ + std::wstring hotKeyStrReparent = + std::wstring(reparentHotkey.win ? L"Win + " : L"") + + std::wstring(reparentHotkey.ctrl ? L"Ctrl + " : L"") + + std::wstring(reparentHotkey.shift ? L"Shift + " : L"") + + std::wstring(reparentHotkey.alt ? L"Alt + " : L"") + + std::wstring(L"VK ") + std::to_wstring(reparentHotkey.key); + + std::wstring hotKeyStrThumbnail = + std::wstring(thumbnailHotkey.win ? L"Win + " : L"") + + std::wstring(thumbnailHotkey.ctrl ? L"Ctrl + " : L"") + + std::wstring(thumbnailHotkey.shift ? L"Shift + " : L"") + + std::wstring(thumbnailHotkey.alt ? L"Alt + " : L"") + + std::wstring(L"VK ") + std::to_wstring(thumbnailHotkey.key); + + TraceLoggingWrite( + g_hProvider, + "CropAndLock_Settings", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingWideString(hotKeyStrReparent.c_str(), "ReparentHotKey"), + TraceLoggingWideString(hotKeyStrThumbnail.c_str(), "ThumbnailHotkey") + ); +} diff --git a/src/modules/CropAndLock/CropAndLock/trace.h b/src/modules/CropAndLock/CropAndLock/trace.h new file mode 100644 index 0000000000..f7e1903ee6 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/trace.h @@ -0,0 +1,20 @@ +#pragma once +#include + +class Trace +{ +public: + static void RegisterProvider() noexcept; + static void UnregisterProvider() noexcept; + + class CropAndLock + { + public: + static void Enable(bool enabled) noexcept; + static void ActivateReparent() noexcept; + static void ActivateThumbnail() noexcept; + static void CreateReparentWindow() noexcept; + static void CreateThumbnailWindow() noexcept; + static void SettingsTelemetry(PowertoyModuleIface::Hotkey&, PowertoyModuleIface::Hotkey&) noexcept; + }; +}; diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.rc b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.rc new file mode 100644 index 0000000000..5fa3c8b90d --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.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 diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj new file mode 100644 index 0000000000..e32228c44d --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj @@ -0,0 +1,118 @@ + + + + + 16.0 + Win32Proj + {3157fa75-86cf-4ee2-8f62-c43f776493c6} + CropAndLockModuleInterface + CropAndLockModuleInterface + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + ..\..\..\..\$(Platform)\$(Configuration)\ + PowerToys.CropAndLockModuleInterface + + + + ..\;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src;%(AdditionalIncludeDirectories) + + + $(OutDir)$(TargetName)$(TargetExt) + %(AdditionalDependencies) + + + + + true + _DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + + + Windows + true + false + + + + + true + true + true + NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + + + Windows + true + true + true + false + + + + + + + + + + + + + + + + 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/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj.filters b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj.filters new file mode 100644 index 0000000000..c249fbe065 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj.filters @@ -0,0 +1,53 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + + + Resource Files + + + + + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/dllmain.cpp b/src/modules/CropAndLock/CropAndLockModuleInterface/dllmain.cpp new file mode 100644 index 0000000000..c313d63cd7 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/dllmain.cpp @@ -0,0 +1,334 @@ +#include "pch.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace NonLocalizable +{ + const wchar_t ModulePath[] = L"PowerToys.CropAndLock.exe"; +} + +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_REPARENT_HOTKEY[] = L"reparent-hotkey"; + const wchar_t JSON_KEY_THUMBNAIL_HOTKEY[] = L"thumbnail-hotkey"; + const wchar_t JSON_KEY_VALUE[] = L"value"; +} + +BOOL APIENTRY DllMain( HMODULE /*hModule*/, + DWORD ul_reason_for_call, + LPVOID /*lpReserved*/ + ) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + Trace::RegisterProvider(); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + case DLL_PROCESS_DETACH: + Trace::UnregisterProvider(); + break; + } + return TRUE; + +} + +class CropAndLockModuleInterface : public PowertoyModuleIface +{ +public: + // Return the localized display name of the powertoy + virtual PCWSTR get_name() override + { + return app_name.c_str(); + } + + // Return the non localized key of the powertoy, this will be cached by the runner + virtual const wchar_t* get_key() override + { + return app_key.c_str(); + } + + // 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::getConfiguredCropAndLockEnabledValue(); + } + + // Return JSON with the configuration options. + // These are the settings shown on the settings page along with their current values. + 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()); + + return settings.serialize_to_buffer(buffer, buffer_size); + } + + // Passes JSON with the configuration settings for the powertoy. + // This is called when the user hits Save on the settings page. + 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. + } + } + + virtual bool on_hotkey(size_t hotkeyId) override + { + if (m_enabled) + { + Logger::trace(L"CropAndLock hotkey pressed"); + if (!is_process_running()) + { + Enable(); + } + + if (hotkeyId == 0) { // Same order as set by get_hotkeys + SetEvent(m_reparent_event_handle); + Trace::CropAndLock::ActivateReparent(); + } + if (hotkeyId == 1) { // Same order as set by get_hotkeys + SetEvent(m_thumbnail_event_handle); + Trace::CropAndLock::ActivateThumbnail(); + } + + return true; + } + + return false; + } + + virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override + { + if (hotkeys && buffer_size >= 2) + { + hotkeys[0] = m_reparent_hotkey; + hotkeys[1] = m_thumbnail_hotkey; + } + return 2; + } + + // Enable the powertoy + virtual void enable() + { + Logger::info("CropAndLock enabling"); + Enable(); + } + + // Disable the powertoy + virtual void disable() + { + Logger::info("CropAndLock disabling"); + Disable(true); + } + + // Returns if the powertoy is enabled + virtual bool is_enabled() override + { + return m_enabled; + } + + // Destroy the powertoy and free memory + virtual void destroy() override + { + Disable(false); + delete this; + } + + virtual void send_settings_telemetry() override + { + Logger::info("Send settings telemetry"); + Trace::CropAndLock::SettingsTelemetry(m_reparent_hotkey, m_thumbnail_hotkey); + } + + CropAndLockModuleInterface() + { + app_name = L"CropAndLock"; + app_key = NonLocalizable::ModuleKey; + LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::cropAndLockLoggerName); + + m_reparent_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT); + m_thumbnail_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_THUMBNAIL_EVENT); + m_exit_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_EXIT_EVENT); + + init_settings(); + } + +private: + void Enable() + { + m_enabled = true; + + // Log telemetry + Trace::CropAndLock::Enable(true); + + // Pass the PID. + unsigned long powertoys_pid = GetCurrentProcessId(); + std::wstring executable_args = L""; + executable_args.append(std::to_wstring(powertoys_pid)); + + ResetEvent(m_reparent_event_handle); + ResetEvent(m_thumbnail_event_handle); + ResetEvent(m_exit_event_handle); + + SHELLEXECUTEINFOW sei{ sizeof(sei) }; + sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI }; + sei.lpFile = NonLocalizable::ModulePath; + sei.nShow = SW_SHOWNORMAL; + sei.lpParameters = executable_args.data(); + if (ShellExecuteExW(&sei) == false) + { + Logger::error(L"Failed to start CropAndLock"); + auto message = get_last_error_message(GetLastError()); + if (message.has_value()) + { + Logger::error(message.value()); + } + } + else + { + m_hProcess = sei.hProcess; + } + + } + + void Disable(bool const traceEvent) + { + m_enabled = false; + + // We can't just kill the process, since Crop and Lock might need to release any reparented windows first. + SetEvent(m_exit_event_handle); + + ResetEvent(m_reparent_event_handle); + ResetEvent(m_thumbnail_event_handle); + + // Log telemetry + if (traceEvent) + { + Trace::CropAndLock::Enable(false); + } + + if (m_hProcess) + { + m_hProcess = nullptr; + } + + } + + void parse_hotkey(PowerToysSettings::PowerToyValues& settings) + { + auto settingsObject = settings.get_raw_json(); + if (settingsObject.GetView().Size()) + { + try + { + Hotkey _temp_reparent; + auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_REPARENT_HOTKEY).GetNamedObject(JSON_KEY_VALUE); + _temp_reparent.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN); + _temp_reparent.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT); + _temp_reparent.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); + _temp_reparent.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); + _temp_reparent.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); + m_reparent_hotkey = _temp_reparent; + } + catch (...) + { + Logger::error("Failed to initialize CropAndLock reparent shortcut from settings. Value will keep unchanged."); + } + try + { + Hotkey _temp_thumbnail; + auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_THUMBNAIL_HOTKEY).GetNamedObject(JSON_KEY_VALUE); + _temp_thumbnail.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN); + _temp_thumbnail.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT); + _temp_thumbnail.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); + _temp_thumbnail.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); + _temp_thumbnail.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); + m_thumbnail_hotkey = _temp_thumbnail; + } + catch (...) + { + Logger::error("Failed to initialize CropAndLock thumbnail shortcut from settings. Value will keep unchanged."); + } + } + else + { + Logger::info("CropAndLock settings are empty"); + } + } + + bool is_process_running() + { + return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT; + } + + void init_settings() + { + try + { + // Load and parse the settings file for this PowerToy. + PowerToysSettings::PowerToyValues settings = + PowerToysSettings::PowerToyValues::load_from_settings_file(get_key()); + + 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. + } + } + + std::wstring app_name; + std::wstring app_key; //contains the non localized key of the powertoy + + bool m_enabled = false; + HANDLE m_hProcess = nullptr; + + // TODO: actual default hotkey setting in line with other PowerToys. + Hotkey m_reparent_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'R' }; + Hotkey m_thumbnail_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'T' }; + + HANDLE m_reparent_event_handle; + HANDLE m_thumbnail_event_handle; + HANDLE m_exit_event_handle; + +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new CropAndLockModuleInterface(); +} diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/packages.config b/src/modules/CropAndLock/CropAndLockModuleInterface/packages.config new file mode 100644 index 0000000000..c92dd4bf0c --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/pch.cpp b/src/modules/CropAndLock/CropAndLockModuleInterface/pch.cpp new file mode 100644 index 0000000000..64b7eef6d6 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/pch.h b/src/modules/CropAndLock/CropAndLockModuleInterface/pch.h new file mode 100644 index 0000000000..0df2e08a6f --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/pch.h @@ -0,0 +1,12 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/resource.h b/src/modules/CropAndLock/CropAndLockModuleInterface/resource.h new file mode 100644 index 0000000000..c2d7221f0e --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/resource.h @@ -0,0 +1,13 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by CropAndLockModuleInterface.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys CropAndLockModuleInterface" +#define INTERNAL_NAME "PowerToys.CropAndLockModuleInterface" +#define ORIGINAL_FILENAME "PowerToys.CropAndLockModuleInterface.dll" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/alwaysontop/AlwaysOnTop/trace.cpp b/src/modules/alwaysontop/AlwaysOnTop/trace.cpp index 591f56d7f7..ae32849bbf 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/trace.cpp +++ b/src/modules/alwaysontop/AlwaysOnTop/trace.cpp @@ -52,4 +52,4 @@ void Trace::AlwaysOnTop::UnpinWindow() noexcept EventUnpinWindowKey, ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); -} \ No newline at end of file +} diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 9d0e1f5e44..6b2db856cb 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -156,6 +156,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"WinUI3Apps/PowerToys.HostsModuleInterface.dll", L"WinUI3Apps/PowerToys.Peek.dll", L"PowerToys.MouseWithoutBordersModuleInterface.dll", + L"PowerToys.CropAndLockModuleInterface.dll", }; const auto VCM_PATH = L"PowerToys.VideoConferenceModule.dll"; if (const auto mf = LoadLibraryA("mf.dll")) diff --git a/src/settings-ui/Settings.UI.Library/CropAndLockProperties.cs b/src/settings-ui/Settings.UI.Library/CropAndLockProperties.cs new file mode 100644 index 0000000000..7a850eebf5 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/CropAndLockProperties.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class CropAndLockProperties + { + public static readonly HotkeySettings DefaultReparentHotkeyValue = new HotkeySettings(true, true, false, true, 0x52); // Ctrl+Win+Shift+R + public static readonly HotkeySettings DefaultThumbnailHotkeyValue = new HotkeySettings(true, true, false, true, 0x54); // Ctrl+Win+Shift+T + + public CropAndLockProperties() + { + ReparentHotkey = new KeyboardKeysProperty(DefaultReparentHotkeyValue); + ThumbnailHotkey = new KeyboardKeysProperty(DefaultThumbnailHotkeyValue); + } + + [JsonPropertyName("reparent-hotkey")] + public KeyboardKeysProperty ReparentHotkey { get; set; } + + [JsonPropertyName("thumbnail-hotkey")] + public KeyboardKeysProperty ThumbnailHotkey { get; set; } + } +} diff --git a/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs new file mode 100644 index 0000000000..2e672c3505 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.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 CropAndLockSettings : BasePTModuleSettings, ISettingsConfig + { + public const string ModuleName = "CropAndLock"; + public const string ModuleVersion = "0.0.1"; + + public CropAndLockSettings() + { + Name = ModuleName; + Version = ModuleVersion; + Properties = new CropAndLockProperties(); + } + + [JsonPropertyName("properties")] + public CropAndLockProperties Properties { get; set; } + + public string GetModuleName() + { + return Name; + } + + public bool UpgradeSettingsConfiguration() + { + return false; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs index a64da60beb..b93ad376b3 100644 --- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs @@ -166,6 +166,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + private bool cropAndLock = true; + + [JsonPropertyName("CropAndLock")] + public bool CropAndLock + { + get => cropAndLock; + set + { + if (cropAndLock != value) + { + LogTelemetryEvent(value); + cropAndLock = value; + NotifyChange(); + } + } + } + private bool awake; [JsonPropertyName("Awake")] diff --git a/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsCropAndLock.png b/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsCropAndLock.png new file mode 100644 index 0000000000..767352a350 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsCropAndLock.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/CropAndLock.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/CropAndLock.png new file mode 100644 index 0000000000..d374783648 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/CropAndLock.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/CropAndLock.gif b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/CropAndLock.gif new file mode 100644 index 0000000000..951392aaf7 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/CropAndLock.gif differ diff --git a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs index 669ee0a5e5..af54f7c679 100644 --- a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs +++ b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs @@ -10,6 +10,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums AlwaysOnTop, Awake, ColorPicker, + CropAndLock, FancyZones, FileLocksmith, FileExplorer, diff --git a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs index 8d1473e687..b41b99ed5b 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs @@ -97,6 +97,9 @@ namespace Microsoft.PowerToys.Settings.UI case "ColorPicker": needToUpdate = generalSettingsConfig.Enabled.ColorPicker != isEnabled; generalSettingsConfig.Enabled.ColorPicker = isEnabled; break; + case "CropAndLock": + needToUpdate = generalSettingsConfig.Enabled.CropAndLock != isEnabled; + generalSettingsConfig.Enabled.CropAndLock = isEnabled; break; case "FancyZones": needToUpdate = generalSettingsConfig.Enabled.FancyZones != isEnabled; generalSettingsConfig.Enabled.FancyZones = isEnabled; break; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml new file mode 100644 index 0000000000..9970e6eea1 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + +