From d9709b2b9103ca789dd824fbf5fa9f85ffffb6cc Mon Sep 17 00:00:00 2001 From: fm-sys <64581222+fm-sys@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:08:17 +0100 Subject: [PATCH] Add non-updating mode for Crop-And-Lock (#40720) ## Summary of the Pull Request Adds a "screenshot" mode to Crop And Lock, which allows creating a window showing a freezed snapshot of the original window. ## PR Checklist - [x] **Closes:** #31799, #33071 (also requested in the already closed duplicate issues #28633, #33812, #37337, ) - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass (crop-and-lock utility doesn't have any tests) - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [x] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [x] **Documentation updated:** https://github.com/MicrosoftDocs/windows-dev-docs/pull/5528 ## Detailed Description of the Pull Request / Additional comments It was asked why this feature is needed at all, because it could be done with snipping tool and just AoT that window as well. While this is true, PowerToys goal always was to improve and speed up workflows. Instead of capturing the screenshot, opening it, and then apply "Crop and Lock" or "Always on Top" on the screenshots window, this PR aims to provide this functionality in a single step. Example use cases: - _when I want to compare between two situations like previous output result and current output result._ (#31799) - _Allow cropping a section of a large code file (say top while working at the bottom) as reference while working elsewhere in the file._ (#33071) - _Can be useful for the work in the same document, like excel or word where you are actively checking the data from the same document._ (#28633) - _In lot's of older applications, if you need to get some information or data from one dialog do another, but because of dialog modality it's not possible to have both windows open at the same time._ (#33812) - _nowadays quite a lot is happening inside the browser. Quite often, I want to keep a small portion of the current website visible and switch to e.g. the writing tool also running in a different tab in the same browser window._ (#31799) I've used win+ctrl+shift+s as the default activation shortcut, as it's not yet used by other powertoys utilities, has similarity with the normal win+shift+s shortcut hotkey and is consistent with the other Crop and Lock shortcuts win+ctrl+shift+r (Reparent Mode) and win+ctrl+shift+t (Thumbnail Mode). ## Validation Steps Performed Compatibility tested manually with a large set of applications I have installed on my computer. However, automated tests don't really make sense as there is not much business logic which could be tested. --------- Co-authored-by: Niels Laute Co-authored-by: vanzue --- .github/actions/spell-check/expect.txt | 1 + doc/devdocs/modules/cropandlock.md | 4 + src/common/interop/Constants.cpp | 4 + src/common/interop/Constants.h | 1 + src/common/interop/Constants.idl | 1 + src/common/interop/shared_constants.h | 1 + .../CropAndLock/CropAndLock.vcxproj | 2 + .../CropAndLock/CropAndLock.vcxproj.filters | 2 + .../ScreenshotCropAndLockWindow.cpp | 178 ++++++++++++++++++ .../CropAndLock/ScreenshotCropAndLockWindow.h | 27 +++ .../CropAndLock/CropAndLock/SettingsWindow.h | 1 + src/modules/CropAndLock/CropAndLock/main.cpp | 29 ++- src/modules/CropAndLock/CropAndLock/trace.cpp | 30 ++- src/modules/CropAndLock/CropAndLock/trace.h | 4 +- .../CropAndLockModuleInterface/dllmain.cpp | 32 +++- .../CropAndLock/CropAndLockReparentCommand.cs | 24 ++- .../CropAndLockScreenshotCommand.cs | 41 ++++ .../CropAndLockThumbnailCommand.cs | 24 ++- .../CropAndLockModuleCommandProvider.cs | 7 + .../Properties/Resources.Designer.cs | 18 ++ .../Properties/Resources.resx | 6 + .../CropAndLockProperties.cs | 5 + .../CropAndLockSettings.cs | 4 + .../OOBE/Views/OobeCropAndLock.xaml | 2 + .../OOBE/Views/OobeCropAndLock.xaml.cs | 2 + .../SettingsXAML/Views/CropAndLockPage.xaml | 6 + .../Settings.UI/Strings/en-us/Resources.resw | 12 ++ .../ViewModels/CropAndLockViewModel.cs | 34 +++- .../ViewModels/DashboardViewModel.cs | 1 + 29 files changed, 474 insertions(+), 29 deletions(-) create mode 100644 src/modules/CropAndLock/CropAndLock/ScreenshotCropAndLockWindow.cpp create mode 100644 src/modules/CropAndLock/CropAndLock/ScreenshotCropAndLockWindow.h create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockScreenshotCommand.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index ab3e43eb47..85f42a267b 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1480,6 +1480,7 @@ remoteip Removelnk renamable RENAMEONCOLLISION +RENDERFULLCONTENT reparented reparenting reportfileaccesses diff --git a/doc/devdocs/modules/cropandlock.md b/doc/devdocs/modules/cropandlock.md index 91f020e3e6..db5e9402cf 100644 --- a/doc/devdocs/modules/cropandlock.md +++ b/doc/devdocs/modules/cropandlock.md @@ -20,6 +20,9 @@ Creates a window showing the selected area of the original window. Changes in th ### Reparent Mode Creates a window that replaces the original window, showing only the selected area. The application is controlled through the cropped window. +### Screenshot Mode +Creates a window showing a freezed snapshot of the original window. + ## Code Structure ### Project Layout @@ -30,6 +33,7 @@ The Crop and Lock module is part of the PowerToys solution. All the logic-relate - **OverlayWindow.cpp**: Thumbnail module type's window concrete implementation. - **ReparentCropAndLockWindow.cpp**: Defines the UI for the reparent mode. - **ChildWindow.cpp**: Reparent module type's window concrete implementation. +- **ScreenshotCropAndLockWindow.cpp**: Defines the UI for the screenshot mode. ## Known Issues diff --git a/src/common/interop/Constants.cpp b/src/common/interop/Constants.cpp index 3e1c339233..fef43de566 100644 --- a/src/common/interop/Constants.cpp +++ b/src/common/interop/Constants.cpp @@ -223,6 +223,10 @@ namespace winrt::PowerToys::Interop::implementation { return CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT; } + hstring Constants::CropAndLockScreenshotEvent() + { + return CommonSharedConstants::CROP_AND_LOCK_SCREENSHOT_EVENT; + } hstring Constants::ShowEnvironmentVariablesSharedEvent() { return CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT; diff --git a/src/common/interop/Constants.h b/src/common/interop/Constants.h index 8c95a09f99..cdd883cc41 100644 --- a/src/common/interop/Constants.h +++ b/src/common/interop/Constants.h @@ -59,6 +59,7 @@ namespace winrt::PowerToys::Interop::implementation static hstring TerminateHostsSharedEvent(); static hstring CropAndLockThumbnailEvent(); static hstring CropAndLockReparentEvent(); + static hstring CropAndLockScreenshotEvent(); static hstring ShowEnvironmentVariablesSharedEvent(); static hstring ShowEnvironmentVariablesAdminSharedEvent(); static hstring WorkspacesLaunchEditorEvent(); diff --git a/src/common/interop/Constants.idl b/src/common/interop/Constants.idl index 97be6c8b7e..abd642b197 100644 --- a/src/common/interop/Constants.idl +++ b/src/common/interop/Constants.idl @@ -56,6 +56,7 @@ namespace PowerToys static String TerminateHostsSharedEvent(); static String CropAndLockThumbnailEvent(); static String CropAndLockReparentEvent(); + static String CropAndLockScreenshotEvent(); static String ShowEnvironmentVariablesSharedEvent(); static String ShowEnvironmentVariablesAdminSharedEvent(); static String WorkspacesLaunchEditorEvent(); diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index 73c4fb7006..118683f24c 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -132,6 +132,7 @@ namespace CommonSharedConstants // 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_SCREENSHOT_EVENT[] = L"Local\\PowerToysCropAndLockScreenshotEvent-ff077ab2-8360-4bd1-864a-637389d35593"; const wchar_t CROP_AND_LOCK_EXIT_EVENT[] = L"Local\\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a"; // Path to the events used by EnvironmentVariables diff --git a/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj index c3e9e4f3f1..dfe9f11b2e 100644 --- a/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj +++ b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj @@ -112,6 +112,7 @@ + @@ -126,6 +127,7 @@ + diff --git a/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj.filters b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj.filters index bea68db119..e906ed2a02 100644 --- a/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj.filters +++ b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj.filters @@ -12,6 +12,7 @@ + @@ -28,6 +29,7 @@ + diff --git a/src/modules/CropAndLock/CropAndLock/ScreenshotCropAndLockWindow.cpp b/src/modules/CropAndLock/CropAndLock/ScreenshotCropAndLockWindow.cpp new file mode 100644 index 0000000000..11afbd0a26 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ScreenshotCropAndLockWindow.cpp @@ -0,0 +1,178 @@ +#include "pch.h" +#include "ScreenshotCropAndLockWindow.h" + +const std::wstring ScreenshotCropAndLockWindow::ClassName = L"CropAndLock.ScreenshotCropAndLockWindow"; +std::once_flag ScreenshotCropAndLockWindowClassRegistration; + +void ScreenshotCropAndLockWindow::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)); +} + +ScreenshotCropAndLockWindow::ScreenshotCropAndLockWindow(std::wstring const& titleString, int width, int height) +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + + std::call_once(ScreenshotCropAndLockWindowClassRegistration, []() { 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); +} + +ScreenshotCropAndLockWindow::~ScreenshotCropAndLockWindow() +{ + DestroyWindow(m_window); +} + +LRESULT ScreenshotCropAndLockWindow::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_PAINT: + if (m_captured && m_bitmap) + { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(m_window, &ps); + HDC memDC = CreateCompatibleDC(hdc); + SelectObject(memDC, m_bitmap.get()); + + RECT clientRect = {}; + GetClientRect(m_window, &clientRect); + int clientWidth = clientRect.right - clientRect.left; + int clientHeight = clientRect.bottom - clientRect.top; + + int srcWidth = m_destRect.right - m_destRect.left; + int srcHeight = m_destRect.bottom - m_destRect.top; + + float srcAspect = static_cast(srcWidth) / srcHeight; + float dstAspect = static_cast(clientWidth) / clientHeight; + + int drawWidth = clientWidth; + int drawHeight = static_cast(clientWidth / srcAspect); + if (dstAspect > srcAspect) + { + drawHeight = clientHeight; + drawWidth = static_cast(clientHeight * srcAspect); + } + + int offsetX = (clientWidth - drawWidth) / 2; + int offsetY = (clientHeight - drawHeight) / 2; + + SetStretchBltMode(hdc, HALFTONE); + StretchBlt(hdc, offsetX, offsetY, drawWidth, drawHeight, memDC, 0, 0, srcWidth, srcHeight, SRCCOPY); + DeleteDC(memDC); + EndPaint(m_window, &ps); + } + break; + default: + return base_type::MessageHandler(message, wparam, lparam); + } + return 0; +} + +void ScreenshotCropAndLockWindow::CropAndLock(HWND windowToCrop, RECT cropRect) +{ + if (m_captured) + { + return; + } + + // Get full window bounds + RECT windowRect{}; + winrt::check_hresult(DwmGetWindowAttribute( + windowToCrop, + DWMWA_EXTENDED_FRAME_BOUNDS, + &windowRect, + sizeof(windowRect))); + + RECT clientRect = ClientAreaInScreenSpace(windowToCrop); + auto offsetX = clientRect.left - windowRect.left; + auto offsetY = clientRect.top - windowRect.top; + + m_sourceRect = { + cropRect.left + offsetX, + cropRect.top + offsetY, + cropRect.right + offsetX, + cropRect.bottom + offsetY + }; + + int fullWidth = windowRect.right - windowRect.left; + int fullHeight = windowRect.bottom - windowRect.top; + + HDC fullDC = CreateCompatibleDC(nullptr); + HDC screenDC = GetDC(nullptr); + HBITMAP fullBitmap = CreateCompatibleBitmap(screenDC, fullWidth, fullHeight); + HGDIOBJ oldFullBitmap = SelectObject(fullDC, fullBitmap); + + // Capture full window + winrt::check_bool(PrintWindow(windowToCrop, fullDC, PW_RENDERFULLCONTENT)); + + + // Crop + int cropWidth = m_sourceRect.right - m_sourceRect.left; + int cropHeight = m_sourceRect.bottom - m_sourceRect.top; + + HDC cropDC = CreateCompatibleDC(nullptr); + HBITMAP cropBitmap = CreateCompatibleBitmap(screenDC, cropWidth, cropHeight); + HGDIOBJ oldCropBitmap = SelectObject(cropDC, cropBitmap); + ReleaseDC(nullptr, screenDC); + + BitBlt( + cropDC, + 0, + 0, + cropWidth, + cropHeight, + fullDC, + m_sourceRect.left, + m_sourceRect.top, + SRCCOPY); + + SelectObject(fullDC, oldFullBitmap); + DeleteObject(fullBitmap); + DeleteDC(fullDC); + + SelectObject(cropDC, oldCropBitmap); + DeleteDC(cropDC); + m_bitmap.reset(cropBitmap); + + // Resize our window + RECT dest{ 0, 0, cropWidth, cropHeight }; + LONG_PTR exStyle = GetWindowLongPtrW(m_window, GWL_EXSTYLE); + LONG_PTR style = GetWindowLongPtrW(m_window, GWL_STYLE); + + winrt::check_bool(AdjustWindowRectEx(&dest, static_cast(style), FALSE, static_cast(exStyle))); + + winrt::check_bool(SetWindowPos( + m_window, HWND_TOPMOST, 0, 0, dest.right - dest.left, dest.bottom - dest.top, SWP_NOMOVE | SWP_SHOWWINDOW)); + + m_destRect = { 0, 0, cropWidth, cropHeight }; + m_captured = true; + InvalidateRect(m_window, nullptr, FALSE); +} \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/ScreenshotCropAndLockWindow.h b/src/modules/CropAndLock/CropAndLock/ScreenshotCropAndLockWindow.h new file mode 100644 index 0000000000..149e4c740a --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ScreenshotCropAndLockWindow.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include "CropAndLockWindow.h" + +struct ScreenshotCropAndLockWindow : robmikh::common::desktop::DesktopWindow, CropAndLockWindow +{ + static const std::wstring ClassName; + ScreenshotCropAndLockWindow(std::wstring const& titleString, int width, int height); + ~ScreenshotCropAndLockWindow() 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(); + +private: + std::unique_ptr m_bitmap{ nullptr, &DeleteObject }; + RECT m_destRect = {}; + RECT m_sourceRect = {}; + + bool m_captured = false; + 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 index 88489601ee..f51e4636f0 100644 --- a/src/modules/CropAndLock/CropAndLock/SettingsWindow.h +++ b/src/modules/CropAndLock/CropAndLock/SettingsWindow.h @@ -4,4 +4,5 @@ enum class CropAndLockType { Reparent, Thumbnail, + Screenshot, }; diff --git a/src/modules/CropAndLock/CropAndLock/main.cpp b/src/modules/CropAndLock/CropAndLock/main.cpp index 5aeea262a4..8f3ac89569 100644 --- a/src/modules/CropAndLock/CropAndLock/main.cpp +++ b/src/modules/CropAndLock/CropAndLock/main.cpp @@ -2,6 +2,7 @@ #include "SettingsWindow.h" #include "OverlayWindow.h" #include "CropAndLockWindow.h" +#include "ScreenshotCropAndLockWindow.h" #include "ThumbnailCropAndLockWindow.h" #include "ReparentCropAndLockWindow.h" #include "ModuleConstants.h" @@ -133,6 +134,7 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I // Handles and thread for the events sent from runner HANDLE m_reparent_event_handle; HANDLE m_thumbnail_event_handle; + HANDLE m_screenshot_event_handle; HANDLE m_exit_event_handle; std::thread m_event_triggers_thread; @@ -181,6 +183,11 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I Logger::trace(L"Creating a thumbnail window"); Trace::CropAndLock::CreateThumbnailWindow(); break; + case CropAndLockType::Screenshot: + croppedWindow = std::make_shared(title, 800, 600); + Logger::trace(L"Creating a screenshot window"); + Trace::CropAndLock::CreateScreenshotWindow(); + break; default: return; } @@ -215,8 +222,9 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I // 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_screenshot_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_SCREENSHOT_EVENT); m_exit_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_EXIT_EVENT); - if (!m_reparent_event_handle || !m_thumbnail_event_handle || !m_exit_event_handle) + if (!m_reparent_event_handle || !m_thumbnail_event_handle || !m_screenshot_event_handle || !m_exit_event_handle) { Logger::warn(L"Failed to create events. {}", get_last_error_or_default(GetLastError())); return 1; @@ -224,10 +232,10 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I m_event_triggers_thread = std::thread([&]() { MSG msg; - HANDLE event_handles[3] = { m_reparent_event_handle, m_thumbnail_event_handle, m_exit_event_handle }; + HANDLE event_handles[4] = { m_reparent_event_handle, m_thumbnail_event_handle, m_screenshot_event_handle, m_exit_event_handle }; while (m_running) { - DWORD dwEvt = MsgWaitForMultipleObjects(3, event_handles, false, INFINITE, QS_ALLINPUT); + DWORD dwEvt = MsgWaitForMultipleObjects(4, event_handles, false, INFINITE, QS_ALLINPUT); if (!m_running) { break; @@ -259,13 +267,25 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I break; } case WAIT_OBJECT_0 + 2: + { + // Screenshot Event + bool enqueueSucceeded = controller.DispatcherQueue().TryEnqueue([&]() { + ProcessCommand(CropAndLockType::Screenshot); + }); + if (!enqueueSucceeded) + { + Logger::error("Couldn't enqueue message to screenshot a window."); + } + break; + } + case WAIT_OBJECT_0 + 3: { // Exit Event Logger::trace(L"Received an exit event."); PostThreadMessage(mainThreadId, WM_QUIT, 0, 0); break; } - case WAIT_OBJECT_0 + 3: + case WAIT_OBJECT_0 + 4: if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); @@ -295,6 +315,7 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I SetEvent(m_reparent_event_handle); CloseHandle(m_reparent_event_handle); CloseHandle(m_thumbnail_event_handle); + CloseHandle(m_screenshot_event_handle); CloseHandle(m_exit_event_handle); m_event_triggers_thread.join(); diff --git a/src/modules/CropAndLock/CropAndLock/trace.cpp b/src/modules/CropAndLock/CropAndLock/trace.cpp index 42674ec624..3a08fb9683 100644 --- a/src/modules/CropAndLock/CropAndLock/trace.cpp +++ b/src/modules/CropAndLock/CropAndLock/trace.cpp @@ -41,6 +41,15 @@ void Trace::CropAndLock::ActivateThumbnail() noexcept TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); } +void Trace::CropAndLock::ActivateScreenshot() noexcept +{ + TraceLoggingWriteWrapper( + g_hProvider, + "CropAndLock_ActivateScreenshot", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + void Trace::CropAndLock::CreateReparentWindow() noexcept { TraceLoggingWriteWrapper( @@ -59,8 +68,17 @@ void Trace::CropAndLock::CreateThumbnailWindow() noexcept TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); } +void Trace::CropAndLock::CreateScreenshotWindow() noexcept +{ + TraceLoggingWriteWrapper( + g_hProvider, + "CropAndLock_CreateScreenshotWindow", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + // Event to send settings telemetry. -void Trace::CropAndLock::SettingsTelemetry(PowertoyModuleIface::Hotkey& reparentHotkey, PowertoyModuleIface::Hotkey& thumbnailHotkey) noexcept +void Trace::CropAndLock::SettingsTelemetry(PowertoyModuleIface::Hotkey& reparentHotkey, PowertoyModuleIface::Hotkey& thumbnailHotkey, PowertoyModuleIface::Hotkey& screenshotHotkey) noexcept { std::wstring hotKeyStrReparent = std::wstring(reparentHotkey.win ? L"Win + " : L"") + @@ -76,11 +94,19 @@ void Trace::CropAndLock::SettingsTelemetry(PowertoyModuleIface::Hotkey& reparent std::wstring(thumbnailHotkey.alt ? L"Alt + " : L"") + std::wstring(L"VK ") + std::to_wstring(thumbnailHotkey.key); + std::wstring hotKeyStrScreenshot = + std::wstring(screenshotHotkey.win ? L"Win + " : L"") + + std::wstring(screenshotHotkey.ctrl ? L"Ctrl + " : L"") + + std::wstring(screenshotHotkey.shift ? L"Shift + " : L"") + + std::wstring(screenshotHotkey.alt ? L"Alt + " : L"") + + std::wstring(L"VK ") + std::to_wstring(screenshotHotkey.key); + TraceLoggingWriteWrapper( g_hProvider, "CropAndLock_Settings", ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), TraceLoggingWideString(hotKeyStrReparent.c_str(), "ReparentHotKey"), - TraceLoggingWideString(hotKeyStrThumbnail.c_str(), "ThumbnailHotkey")); + TraceLoggingWideString(hotKeyStrThumbnail.c_str(), "ThumbnailHotkey"), + TraceLoggingWideString(hotKeyStrScreenshot.c_str(), "ScreenshotHotkey")); } diff --git a/src/modules/CropAndLock/CropAndLock/trace.h b/src/modules/CropAndLock/CropAndLock/trace.h index 5a9aaa95ca..bd9a3431a2 100644 --- a/src/modules/CropAndLock/CropAndLock/trace.h +++ b/src/modules/CropAndLock/CropAndLock/trace.h @@ -12,8 +12,10 @@ public: static void Enable(bool enabled) noexcept; static void ActivateReparent() noexcept; static void ActivateThumbnail() noexcept; + static void ActivateScreenshot() noexcept; static void CreateReparentWindow() noexcept; static void CreateThumbnailWindow() noexcept; - static void SettingsTelemetry(PowertoyModuleIface::Hotkey&, PowertoyModuleIface::Hotkey&) noexcept; + static void CreateScreenshotWindow() noexcept; + static void SettingsTelemetry(PowertoyModuleIface::Hotkey&, PowertoyModuleIface::Hotkey&, PowertoyModuleIface::Hotkey&) noexcept; }; }; diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/dllmain.cpp b/src/modules/CropAndLock/CropAndLockModuleInterface/dllmain.cpp index 42c7c6da7e..9821b786f1 100644 --- a/src/modules/CropAndLock/CropAndLockModuleInterface/dllmain.cpp +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/dllmain.cpp @@ -29,6 +29,7 @@ namespace 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_SCREENSHOT_HOTKEY[] = L"screenshot-hotkey"; const wchar_t JSON_KEY_VALUE[] = L"value"; } @@ -124,6 +125,10 @@ public: SetEvent(m_thumbnail_event_handle); Trace::CropAndLock::ActivateThumbnail(); } + if (hotkeyId == 2) { // Same order as set by get_hotkeys + SetEvent(m_screenshot_event_handle); + Trace::CropAndLock::ActivateScreenshot(); + } return true; } @@ -133,12 +138,13 @@ public: virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override { - if (hotkeys && buffer_size >= 2) + if (hotkeys && buffer_size >= 3) { hotkeys[0] = m_reparent_hotkey; hotkeys[1] = m_thumbnail_hotkey; + hotkeys[2] = m_screenshot_hotkey; } - return 2; + return 3; } // Enable the powertoy @@ -171,7 +177,7 @@ public: virtual void send_settings_telemetry() override { Logger::info("Send settings telemetry"); - Trace::CropAndLock::SettingsTelemetry(m_reparent_hotkey, m_thumbnail_hotkey); + Trace::CropAndLock::SettingsTelemetry(m_reparent_hotkey, m_thumbnail_hotkey, m_screenshot_hotkey); } CropAndLockModuleInterface() @@ -182,6 +188,7 @@ public: m_reparent_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT); m_thumbnail_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_THUMBNAIL_EVENT); + m_screenshot_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_SCREENSHOT_EVENT); m_exit_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_EXIT_EVENT); init_settings(); @@ -202,6 +209,7 @@ private: ResetEvent(m_reparent_event_handle); ResetEvent(m_thumbnail_event_handle); + ResetEvent(m_screenshot_event_handle); ResetEvent(m_exit_event_handle); SHELLEXECUTEINFOW sei{ sizeof(sei) }; @@ -234,6 +242,7 @@ private: ResetEvent(m_reparent_event_handle); ResetEvent(m_thumbnail_event_handle); + ResetEvent(m_screenshot_event_handle); // Log telemetry if (traceEvent) @@ -283,6 +292,21 @@ private: { Logger::error("Failed to initialize CropAndLock thumbnail shortcut from settings. Value will keep unchanged."); } + try + { + Hotkey _temp_screenshot; + auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SCREENSHOT_HOTKEY).GetNamedObject(JSON_KEY_VALUE); + _temp_screenshot.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN); + _temp_screenshot.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT); + _temp_screenshot.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); + _temp_screenshot.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); + _temp_screenshot.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); + m_screenshot_hotkey = _temp_screenshot; + } + catch (...) + { + Logger::error("Failed to initialize CropAndLock screenshot shortcut from settings. Value will keep unchanged."); + } } else { @@ -321,9 +345,11 @@ private: // TODO: actual default hotkey setting in line with other PowerToys. Hotkey m_reparent_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'R' }; Hotkey m_thumbnail_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'T' }; + Hotkey m_screenshot_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'S' }; HANDLE m_reparent_event_handle; HANDLE m_thumbnail_event_handle; + HANDLE m_screenshot_event_handle; HANDLE m_exit_event_handle; }; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockReparentCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockReparentCommand.cs index 417ab34a5d..2c7bfe6868 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockReparentCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockReparentCommand.cs @@ -4,6 +4,7 @@ using System; using System.Threading; +using System.Threading.Tasks; using Microsoft.CommandPalette.Extensions.Toolkit; using PowerToys.Interop; @@ -21,15 +22,20 @@ internal sealed partial class CropAndLockReparentCommand : InvokableCommand public override CommandResult Invoke() { - try + Task.Run(async () => { - using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockReparentEvent()); - evt.Set(); - return CommandResult.Dismiss(); - } - catch (Exception ex) - { - return CommandResult.ShowToast($"Failed to start Crop and Lock (Reparent): {ex.Message}"); - } + await Task.Delay(500); + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockReparentEvent()); + evt.Set(); + } + catch + { + // Ignore errors after dismissing + } + }); + + return CommandResult.Dismiss(); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockScreenshotCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockScreenshotCommand.cs new file mode 100644 index 0000000000..1b6e295144 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockScreenshotCommand.cs @@ -0,0 +1,41 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Triggers Crop and Lock screenshot mode via the shared event. +/// +internal sealed partial class CropAndLockScreenshotCommand : InvokableCommand +{ + public CropAndLockScreenshotCommand() + { + Name = "Crop and Lock (Screenshot)"; + } + + public override CommandResult Invoke() + { + Task.Run(async () => + { + await Task.Delay(500); + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockScreenshotEvent()); + evt.Set(); + } + catch + { + // Ignore errors after dismissing + } + }); + + return CommandResult.Dismiss(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockThumbnailCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockThumbnailCommand.cs index b9996f7835..7b1ce62e56 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockThumbnailCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/CropAndLock/CropAndLockThumbnailCommand.cs @@ -4,6 +4,7 @@ using System; using System.Threading; +using System.Threading.Tasks; using Microsoft.CommandPalette.Extensions.Toolkit; using PowerToys.Interop; @@ -21,15 +22,20 @@ internal sealed partial class CropAndLockThumbnailCommand : InvokableCommand public override CommandResult Invoke() { - try + Task.Run(async () => { - using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockThumbnailEvent()); - evt.Set(); - return CommandResult.Dismiss(); - } - catch (Exception ex) - { - return CommandResult.ShowToast($"Failed to start Crop and Lock (Thumbnail): {ex.Message}"); - } + await Task.Delay(500); + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.CropAndLockThumbnailEvent()); + evt.Set(); + } + catch + { + // Ignore errors after dismissing + } + }); + + return CommandResult.Dismiss(); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CropAndLockModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CropAndLockModuleCommandProvider.cs index 9902735871..e39eb8ebef 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CropAndLockModuleCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/CropAndLockModuleCommandProvider.cs @@ -34,6 +34,13 @@ internal sealed class CropAndLockModuleCommandProvider : ModuleCommandProvider Subtitle = Resources.CropAndLock_Thumbnail_Subtitle, Icon = icon, }; + + yield return new ListItem(new CropAndLockScreenshotCommand()) + { + Title = Resources.CropAndLock_Screenshot_Title, + Subtitle = Resources.CropAndLock_Screenshot_Subtitle, + Icon = icon, + }; } yield return new ListItem(new OpenInSettingsCommand(module, title)) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.Designer.cs index 8b45d1dd2c..d561fcd7a4 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.Designer.cs @@ -375,6 +375,24 @@ namespace PowerToysExtension.Properties { } } + /// + /// Looks up a localized string similar to Crop and Lock (Screenshot). + /// + internal static string CropAndLock_Screenshot_Title { + get { + return ResourceManager.GetString("CropAndLock_Screenshot_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create a cropped screenshot window. + /// + internal static string CropAndLock_Screenshot_Subtitle { + get { + return ResourceManager.GetString("CropAndLock_Screenshot_Subtitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Launch Environment Variables editor. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.resx index 4a70840dfa..acd99a55fc 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.resx @@ -260,6 +260,12 @@ Create a cropped thumbnail window + + Crop and Lock (Screenshot) + + + Create a cropped screenshot window + Open Crop and Lock settings diff --git a/src/settings-ui/Settings.UI.Library/CropAndLockProperties.cs b/src/settings-ui/Settings.UI.Library/CropAndLockProperties.cs index 7a850eebf5..df00c4c6d5 100644 --- a/src/settings-ui/Settings.UI.Library/CropAndLockProperties.cs +++ b/src/settings-ui/Settings.UI.Library/CropAndLockProperties.cs @@ -11,11 +11,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library { public static readonly HotkeySettings DefaultReparentHotkeyValue = new HotkeySettings(true, true, false, true, 0x52); // Ctrl+Win+Shift+R public static readonly HotkeySettings DefaultThumbnailHotkeyValue = new HotkeySettings(true, true, false, true, 0x54); // Ctrl+Win+Shift+T + public static readonly HotkeySettings DefaultScreenshotHotkeyValue = new HotkeySettings(true, true, false, true, 0x53); // Ctrl+Win+Shift+S public CropAndLockProperties() { ReparentHotkey = new KeyboardKeysProperty(DefaultReparentHotkeyValue); ThumbnailHotkey = new KeyboardKeysProperty(DefaultThumbnailHotkeyValue); + ScreenshotHotkey = new KeyboardKeysProperty(DefaultScreenshotHotkeyValue); } [JsonPropertyName("reparent-hotkey")] @@ -23,5 +25,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("thumbnail-hotkey")] public KeyboardKeysProperty ThumbnailHotkey { get; set; } + + [JsonPropertyName("screenshot-hotkey")] + public KeyboardKeysProperty ScreenshotHotkey { get; set; } } } diff --git a/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs index 517c4e8754..bb979d8ecf 100644 --- a/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs +++ b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs @@ -44,6 +44,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library () => Properties.ThumbnailHotkey.Value, value => Properties.ThumbnailHotkey.Value = value ?? CropAndLockProperties.DefaultThumbnailHotkeyValue, "CropAndLock_ThumbnailActivation_Shortcut"), + new HotkeyAccessor( + () => Properties.ScreenshotHotkey.Value, + value => Properties.ScreenshotHotkey.Value = value ?? CropAndLockProperties.DefaultScreenshotHotkeyValue, + "CropAndLock_ScreenshotActivation_Shortcut"), }; return hotkeyAccessors.ToArray(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml index ccea7ff980..cb32790fb3 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml @@ -16,6 +16,8 @@ + +