diff --git a/src/modules/ZoomIt/ZoomIt/CaptureFrameWait.cpp b/src/modules/ZoomIt/ZoomIt/CaptureFrameWait.cpp index 0df8b7db3c8b..58f0185568bd 100644 --- a/src/modules/ZoomIt/ZoomIt/CaptureFrameWait.cpp +++ b/src/modules/ZoomIt/ZoomIt/CaptureFrameWait.cpp @@ -33,7 +33,8 @@ namespace util CaptureFrameWait::CaptureFrameWait( winrt::IDirect3DDevice const& device, winrt::GraphicsCaptureItem const& item, - winrt::SizeInt32 const& size) + winrt::SizeInt32 const& size, + bool borderRequired) { m_device = device; m_item = item; @@ -49,6 +50,11 @@ CaptureFrameWait::CaptureFrameWait( size); m_session = m_framePool.CreateCaptureSession(m_item); + if( !borderRequired ) + { + ShowCaptureBorder( false ); + } + m_framePool.FrameArrived({ this, &CaptureFrameWait::OnFrameArrived }); m_session.StartCapture(); } diff --git a/src/modules/ZoomIt/ZoomIt/CaptureFrameWait.h b/src/modules/ZoomIt/ZoomIt/CaptureFrameWait.h index 08c49ba754b6..e8e357671ce4 100644 --- a/src/modules/ZoomIt/ZoomIt/CaptureFrameWait.h +++ b/src/modules/ZoomIt/ZoomIt/CaptureFrameWait.h @@ -100,10 +100,14 @@ struct CaptureFrame class CaptureFrameWait { public: + // borderRequired false suppresses the system capture border before the + // session starts; the process must already hold borderless capture + // access for it to take effect. CaptureFrameWait( winrt::Direct3D11::IDirect3DDevice const& device, winrt::GraphicsCaptureItem const& item, - winrt::SizeInt32 const& size ); + winrt::SizeInt32 const& size, + bool borderRequired = true ); ~CaptureFrameWait(); std::optional TryGetNextFrame(); @@ -124,6 +128,11 @@ class CaptureFrameWait m_session.IsBorderRequired( show ); } } + void RecreateFramePool( winrt::SizeInt32 const& size ) + { + auto lock = m_lock.lock_exclusive(); + m_framePool.Recreate( m_device, winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized, 1, size ); + } private: void OnFrameArrived( diff --git a/src/modules/ZoomIt/ZoomIt/MirrorWindow.cpp b/src/modules/ZoomIt/ZoomIt/MirrorWindow.cpp new file mode 100644 index 000000000000..ed7fcd452716 --- /dev/null +++ b/src/modules/ZoomIt/ZoomIt/MirrorWindow.cpp @@ -0,0 +1,822 @@ +//============================================================================== +// +// Zoomit +// Sysinternals - www.sysinternals.com +// +// DemoMirror: mirrors a screen region or window, including the mouse cursor, +// onto a second monitor so an audience can follow a demo app on the +// presentation monitor without the presenter leaving the presentation. +// +// Frames come from Windows.Graphics.Capture (CaptureFrameWait) with cursor +// capture enabled, get cached in a texture, and the visible sub-rectangle is +// copied into a flip-model swapchain whose source size drives the zoom: DXGI +// stretches the sub-rectangle to the window with linear filtering. +// +//============================================================================== +#include "pch.h" +#include "MirrorWindow.h" +#include "Utility.h" +#include + +namespace util +{ + using namespace robmikh::common::desktop; +} + +const DWORD MIRROR_FRAME_TIMEOUT_MS = 33; +const UINT MIRROR_TOPMOST_TIMER_MS = 250; + +//---------------------------------------------------------------------------- +// +// RequestBorderlessCapture +// +// Windows 11 draws a yellow system border around captured windows and +// monitors unless the process requests borderless capture access, which is +// granted without a prompt for desktop apps. Until the request completes, +// IsBorderRequired(false) has no effect. Must be called from an MTA thread +// because it blocks on the request. +// +//---------------------------------------------------------------------------- +static void RequestBorderlessCapture() +{ + static bool requested = false; + if( requested ) + { + return; + } + requested = true; + + try + { + if( winrt::ApiInformation::IsTypePresent( L"Windows.Graphics.Capture.GraphicsCaptureAccess" ) ) + { + auto status = winrt::GraphicsCaptureAccess::RequestAccessAsync( winrt::GraphicsCaptureAccessKind::Borderless ).get(); + wchar_t message[128]; + swprintf_s( message, L"[Mirror] Borderless capture access status=%d\n", static_cast( status ) ); + OutputDebugStringW( message ); + } + else + { + OutputDebugStringW( L"[Mirror] GraphicsCaptureAccess type not present\n" ); + } + } + catch( const winrt::hresult_error& error ) + { + wchar_t message[256]; + swprintf_s( message, L"[Mirror] Borderless capture access request failed: 0x%08X\n", + static_cast( error.code().value ) ); + OutputDebugStringW( message ); + } + catch( ... ) + { + OutputDebugStringW( L"[Mirror] Borderless capture access request failed\n" ); + } +} + +//---------------------------------------------------------------------------- +// +// GetMonitorRect +// +//---------------------------------------------------------------------------- +static RECT GetMonitorRect( HMONITOR monitor ) +{ + MONITORINFO monitorInfo = { sizeof( monitorInfo ) }; + GetMonitorInfo( monitor, &monitorInfo ); + return monitorInfo.rcMonitor; +} + +//---------------------------------------------------------------------------- +// +// MirrorWindow::GetWindowFrameRect +// +// GetWindowRect includes the invisible resize borders around modern +// windows, which would mirror as a margin of desktop; the DWM extended +// frame bounds are the visible edges. +// +//---------------------------------------------------------------------------- +RECT MirrorWindow::GetWindowFrameRect( HWND window ) +{ + typedef HRESULT ( WINAPI *type_pDwmGetWindowAttribute )( HWND, DWORD, PVOID, DWORD ); + static type_pDwmGetWindowAttribute pDwmGetWindowAttributeDynamic = + reinterpret_cast( + GetProcAddress( LoadLibraryW( L"dwmapi.dll" ), "DwmGetWindowAttribute" ) ); + + RECT rect{}; + if( pDwmGetWindowAttributeDynamic == nullptr || + FAILED( pDwmGetWindowAttributeDynamic( window, DWMWA_EXTENDED_FRAME_BOUNDS, &rect, sizeof( rect ) ) ) ) + { + GetWindowRect( window, &rect ); + } + return rect; +} + +//---------------------------------------------------------------------------- +// +// MirrorWindow::Start +// +// Creates the mirror window on the target monitor and starts capturing and +// presenting the source. +// +//---------------------------------------------------------------------------- +bool MirrorWindow::Start( winrt::GraphicsCaptureItem const& item, RECT sourceRect, + HWND sourceWindow, HMONITOR sourceMonitor, + HMONITOR targetMonitor, HWND notifyWindow, + std::function annotationQuery, + bool trackWindow, HWND sourceBorderWindow ) +{ + if( IsActive() ) + { + return false; + } + + m_sourceRect = sourceRect; + m_sourceWindow = sourceWindow; + m_sourceBorderWindow = sourceBorderWindow; + m_trackWindow = trackWindow; + m_notifyWindow = notifyWindow; + m_annotationQuery = annotationQuery; + m_annotationState = AnnotationState::None; + m_monitorOverride = false; + m_sourceTexture = nullptr; + m_contentSize = item.Size(); + m_poolSize = item.Size(); + m_sourceMonitor = sourceMonitor; + m_targetMonitor = targetMonitor; + m_targetRect = GetMonitorRect( targetMonitor ); + m_sourceMonitorRect = GetMonitorRect( sourceMonitor ); + + // The mirrored region in capture-texture coordinates: monitor captures + // are relative to the monitor origin, window captures use the full + // captured content. Window tracking captures the monitor and recomputes + // the crop from the window rect every frame. + if( m_trackWindow || m_sourceWindow == nullptr ) + { + SetRect( &m_textureCrop, + sourceRect.left - m_sourceMonitorRect.left, + sourceRect.top - m_sourceMonitorRect.top, + sourceRect.right - m_sourceMonitorRect.left, + sourceRect.bottom - m_sourceMonitorRect.top ); + } + else + { + SetRect( &m_textureCrop, 0, 0, m_contentSize.Width, m_contentSize.Height ); + } + + // The swapchain covers the full capture; the displayed image is the + // crop, which drives the mirror window's aspect ratio. + m_bufferWidth = m_trackWindow ? m_contentSize.Width : ( m_textureCrop.right - m_textureCrop.left ); + m_bufferHeight = m_trackWindow ? m_contentSize.Height : ( m_textureCrop.bottom - m_textureCrop.top ); + m_imageWidth = m_textureCrop.right - m_textureCrop.left; + m_imageHeight = m_textureCrop.bottom - m_textureCrop.top; + if( m_bufferWidth <= 0 || m_bufferHeight <= 0 || m_imageWidth <= 0 || m_imageHeight <= 0 ) + { + return false; + } + + RECT windowRect = ComputeWindowRect(); + + try + { + // Acquire the borderless-capture grant before creating the session + // so the yellow system capture border never appears. The request + // blocks, and needs an MTA thread. + std::thread( []() { + winrt::init_apartment( winrt::apartment_type::multi_threaded ); + RequestBorderlessCapture(); + winrt::uninit_apartment(); + } ).join(); + + // A dedicated D3D device keeps mirroring independent of any + // simultaneous recording session. + m_device = util::CreateD3D11Device(); + m_device->GetImmediateContext( m_context.put() ); + auto multithread = m_context.try_as(); + if( multithread ) + { + multithread->SetMultithreadProtected( TRUE ); + } + + auto dxgiDevice = m_device.as(); + m_winrtDevice = CreateDirect3DDevice( dxgiDevice.get() ); + + m_frameWait = std::make_unique( m_winrtDevice, item, item.Size(), false ); + m_frameWait->EnableCursorCapture( true ); + + WNDCLASSW windowClass{}; + windowClass.lpfnWndProc = []( HWND window, UINT message, WPARAM wordParam, LPARAM longParam ) -> LRESULT + { + if( message == WM_NCCREATE ) + { + auto createStruct = reinterpret_cast( longParam ); + SetWindowLongPtrW( window, GWLP_USERDATA, reinterpret_cast( createStruct->lpCreateParams ) ); + return TRUE; + } + + auto self = reinterpret_cast( GetWindowLongPtrW( window, GWLP_USERDATA ) ); + if( self == nullptr ) + { + return DefWindowProcW( window, message, wordParam, longParam ); + } + return self->WindowProc( window, message, wordParam, longParam ); + }; + windowClass.hInstance = GetModuleHandle( nullptr ); + windowClass.hCursor = LoadCursorW( nullptr, IDC_ARROW ); + windowClass.hbrBackground = static_cast( GetStockObject( BLACK_BRUSH ) ); + windowClass.lpszClassName = m_className; + if( RegisterClassW( &windowClass ) == 0 ) + { + THROW_LAST_ERROR_IF( GetLastError() != ERROR_CLASS_ALREADY_EXISTS ); + } + + // Full-monitor black backdrop behind the mirror so letterbox areas + // don't show the presentation. Created first so the mirror window, + // created after it, starts out above it. + m_backdropWindow = CreateWindowExW( WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TRANSPARENT, + m_className, L"ZoomIt DemoMirror Backdrop", WS_POPUP, + m_targetRect.left, m_targetRect.top, + m_targetRect.right - m_targetRect.left, + m_targetRect.bottom - m_targetRect.top, + nullptr, nullptr, GetModuleHandle( nullptr ), nullptr ); + THROW_LAST_ERROR_IF_NULL( m_backdropWindow ); + + // No activation and click-through: the mirror is display-only and + // must never steal focus from the demo app. + m_window = CreateWindowExW( WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TRANSPARENT, + m_className, L"ZoomIt DemoMirror", WS_POPUP, + windowRect.left, windowRect.top, + windowRect.right - windowRect.left, + windowRect.bottom - windowRect.top, + nullptr, nullptr, GetModuleHandle( nullptr ), this ); + THROW_LAST_ERROR_IF_NULL( m_window ); + + // The mirror and backdrop stay visible to captures so screen + // sharing (e.g. Teams) of the target monitor shows the mirrored + // content. Self-capture can't happen: the target monitor is never + // the source, and the annotation override below pins monitor + // capture to the source monitor. + + DXGI_SWAP_CHAIN_DESC1 swapChainDesc{}; + swapChainDesc.Width = m_bufferWidth; + swapChainDesc.Height = m_bufferHeight; + swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + swapChainDesc.SampleDesc.Count = 1; + swapChainDesc.BufferCount = 2; + swapChainDesc.Scaling = DXGI_SCALING_STRETCH; + swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; + swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + + auto dxgiDevice2 = m_device.as(); + winrt::com_ptr adapter; + winrt::check_hresult( dxgiDevice2->GetParent( winrt::guid_of(), adapter.put_void() ) ); + winrt::com_ptr factory; + winrt::check_hresult( adapter->GetParent( winrt::guid_of(), factory.put_void() ) ); + + winrt::com_ptr swapChain; + winrt::check_hresult( factory->CreateSwapChainForHwnd( m_device.get(), m_window, &swapChainDesc, + nullptr, nullptr, swapChain.put() ) ); + m_swapChain = swapChain.as(); + factory->MakeWindowAssociation( m_window, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES ); + + if( m_sourceWindow != nullptr ) + { + // Bright green border around the mirrored window so the + // presenter can see what's being mirrored, matching the record + // border's width but fully opaque and distinct in color. It + // follows the window as it moves and resizes. + m_borderWindow = CreateWindowExW( WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED, + m_className, L"ZoomIt DemoMirror Border", WS_POPUP, + 0, 0, 0, 0, + nullptr, nullptr, GetModuleHandle( nullptr ), this ); + THROW_LAST_ERROR_IF_NULL( m_borderWindow ); + SetLayeredWindowAttributes( m_borderWindow, 0, 255, LWA_ALPHA ); + EnableWindow( m_borderWindow, FALSE ); + SetWindowDisplayAffinity( m_borderWindow, WDA_EXCLUDEFROMCAPTURE ); + m_borderTarget = m_sourceRect; + UpdateBorderWindow(); + } + } + catch( ... ) + { + Stop(); + return false; + } + + ShowWindow( m_backdropWindow, SW_SHOWNA ); + ShowWindow( m_window, SW_SHOWNA ); + if( m_borderWindow != nullptr ) + { + ShowWindow( m_borderWindow, SW_SHOWNA ); + } + + // The presentation reasserts topmost when a slide show starts, so + // periodically reclaim it like live zoom does. + SetTimer( m_window, TOPMOST_TIMER_ID, MIRROR_TOPMOST_TIMER_MS, nullptr ); + + m_stopEvent.ResetEvent(); + m_renderThread = std::thread( &MirrorWindow::RenderLoop, this ); + return true; +} + +//---------------------------------------------------------------------------- +// +// MirrorWindow::Stop +// +// Stops the capture and render thread and tears down the window. Also +// handles partially-initialized state when Start fails. +// +//---------------------------------------------------------------------------- +void MirrorWindow::Stop() +{ + m_stopEvent.SetEvent(); + if( m_frameWait ) + { + m_frameWait->StopCapture(); + } + if( m_renderThread.joinable() ) + { + m_renderThread.join(); + } + + if( m_window != nullptr ) + { + KillTimer( m_window, TOPMOST_TIMER_ID ); + DestroyWindow( m_window ); + m_window = nullptr; + } + if( m_backdropWindow != nullptr ) + { + DestroyWindow( m_backdropWindow ); + m_backdropWindow = nullptr; + } + if( m_borderWindow != nullptr ) + { + DestroyWindow( m_borderWindow ); + m_borderWindow = nullptr; + } + + m_frameWait = nullptr; + m_sourceTexture = nullptr; + m_swapChain = nullptr; + m_context = nullptr; + m_device = nullptr; + m_winrtDevice = nullptr; + m_sourceWindow = nullptr; + m_sourceBorderWindow = nullptr; + m_sourceMonitor = nullptr; + m_targetMonitor = nullptr; + m_notifyWindow = nullptr; + m_annotationQuery = nullptr; + m_annotationState = AnnotationState::None; + m_monitorOverride = false; + m_trackWindow = false; +} + +//---------------------------------------------------------------------------- +// +// MirrorWindow::SwitchCapture +// +// Replaces the capture source mid-mirror, resizing the swapchain to the new +// content size and re-fitting the mirror window. +// +//---------------------------------------------------------------------------- +bool MirrorWindow::SwitchCapture( winrt::GraphicsCaptureItem const& item, bool enableCursor ) +{ + try + { + auto frameWait = std::make_unique( m_winrtDevice, item, item.Size(), false ); + frameWait->EnableCursorCapture( enableCursor ); + m_frameWait->StopCapture(); + m_frameWait = std::move( frameWait ); + } + catch( ... ) + { + return false; + } + + m_poolSize = item.Size(); + m_bufferWidth = m_poolSize.Width; + m_bufferHeight = m_poolSize.Height; + m_imageWidth = m_bufferWidth; + m_imageHeight = m_bufferHeight; + m_contentSize = m_poolSize; + m_sourceTexture = nullptr; + if( FAILED( m_swapChain->ResizeBuffers( 2, m_bufferWidth, m_bufferHeight, + DXGI_FORMAT_B8G8R8A8_UNORM, 0 ) ) ) + { + return false; + } + PostMessage( m_window, WM_MIRROR_RELAYOUT, 0, 0 ); + return true; +} + +//---------------------------------------------------------------------------- +// +// MirrorWindow::UpdateAnnotationState +// +// While mirroring a window, ZoomIt's zoom/draw/live-zoom overlays aren't +// part of the window's surface, so they wouldn't show in the mirror. When +// one of those modes activates, temporarily mirror the monitor under the +// cursor (where the annotation UI appears) instead, and switch back to the +// window when the mode exits. The target monitor is never captured — the +// mirror is capture-visible and would feed back into itself — so the +// source monitor stands in when the cursor is there. Returns true when the +// capture was switched so the caller can discard the frame from the +// previous capture. +// +//---------------------------------------------------------------------------- +bool MirrorWindow::UpdateAnnotationState() +{ + const AnnotationState state = m_annotationQuery(); + if( state == m_annotationState ) + { + return false; + } + bool switched = false; + + if( m_trackWindow || m_sourceWindow == nullptr ) + { + // Monitor captures (screen, region, and window tracking) show + // annotations in place; just avoid a doubled cursor while live zoom + // renders a magnified one. + m_frameWait->EnableCursorCapture( state != AnnotationState::AnnotatingLiveZoom ); + m_annotationState = state; + return false; + } + + if( state == AnnotationState::None ) + { + if( IsWindow( m_sourceWindow ) ) + { + try + { + auto item = util::CreateCaptureItemForWindow( m_sourceWindow ); + if( SwitchCapture( item, true ) ) + { + m_monitorOverride = false; + switched = true; + } + } + catch( ... ) {} + } + } + else if( !m_monitorOverride ) + { + POINT cursorPos; + GetCursorPos( &cursorPos ); + HMONITOR monitor = MonitorFromPoint( cursorPos, MONITOR_DEFAULTTONEAREST ); + + // The mirror is visible to captures, so capturing the target + // monitor would feed the mirror back into itself; the annotation + // UI is on the source monitor anyway. + if( monitor == m_targetMonitor ) + { + monitor = m_sourceMonitor; + } + try + { + auto item = util::CreateCaptureItemForMonitor( monitor ); + + // Live zoom already renders a magnified cursor into the frame; + // capturing the cursor too would double it. + if( SwitchCapture( item, state != AnnotationState::AnnotatingLiveZoom ) ) + { + m_monitorOverride = true; + switched = true; + } + } + catch( ... ) {} + } + else + { + // Already mirroring the monitor; just track the cursor-capture state. + m_frameWait->EnableCursorCapture( state != AnnotationState::AnnotatingLiveZoom ); + } + m_annotationState = state; + return switched; +} + +//---------------------------------------------------------------------------- +// +// MirrorWindow::ComputeWindowRect +// +// The largest rectangle with the source's aspect ratio that fits on the +// target monitor, centered. +// +//---------------------------------------------------------------------------- +RECT MirrorWindow::ComputeWindowRect() const +{ + const int monitorWidth = m_targetRect.right - m_targetRect.left; + const int monitorHeight = m_targetRect.bottom - m_targetRect.top; + double scale = min( static_cast( monitorWidth ) / m_imageWidth, + static_cast( monitorHeight ) / m_imageHeight ); + + // Mirror windows at native size, scaling only when they don't fit on + // the target monitor, so the image stays stable as the window resizes. + // Regions always fill the monitor. + if( m_sourceWindow != nullptr ) + { + scale = min( scale, 1.0 ); + } + + const int windowWidth = max( 1, static_cast( m_imageWidth * scale ) ); + const int windowHeight = max( 1, static_cast( m_imageHeight * scale ) ); + + RECT windowRect; + windowRect.left = m_targetRect.left + ( monitorWidth - windowWidth ) / 2; + windowRect.top = m_targetRect.top + ( monitorHeight - windowHeight ) / 2; + windowRect.right = windowRect.left + windowWidth; + windowRect.bottom = windowRect.top + windowHeight; + return windowRect; +} + +//---------------------------------------------------------------------------- +// +// MirrorWindow::UpdateBorderWindow +// +// Positions the border frame just outside the source window rectangle, +// using a window region so only the frame is visible. +// +//---------------------------------------------------------------------------- +void MirrorWindow::UpdateBorderWindow() +{ + if( m_borderWindow == nullptr ) + { + return; + } + + const RECT target = m_borderTarget; + const int width = ScaleForDpi( 2, GetDpiForWindowHelper( m_borderWindow ) ); + RECT outer = target; + InflateRect( &outer, width, width ); + + wil::unique_hrgn region{ CreateRectRgn( 0, 0, outer.right - outer.left, outer.bottom - outer.top ) }; + wil::unique_hrgn inside{ CreateRectRgn( width, width, + width + ( target.right - target.left ), + width + ( target.bottom - target.top ) ) }; + CombineRgn( region.get(), region.get(), inside.get(), RGN_XOR ); + + SetWindowPos( m_borderWindow, HWND_TOPMOST, outer.left, outer.top, + outer.right - outer.left, outer.bottom - outer.top, SWP_NOACTIVATE ); + SetWindowRgn( m_borderWindow, region.release(), TRUE ); + RedrawWindow( m_borderWindow, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_FRAME ); +} + +//---------------------------------------------------------------------------- +// +// MirrorWindow::WindowProc +// +//---------------------------------------------------------------------------- +LRESULT MirrorWindow::WindowProc( HWND window, UINT message, WPARAM wordParam, LPARAM longParam ) +{ + switch( message ) + { + case WM_ERASEBKGND: + if( window == m_borderWindow ) + { + RECT clientRect; + GetClientRect( window, &clientRect ); + HBRUSH brush = CreateSolidBrush( MIRROR_BORDER_COLOR ); + FillRect( reinterpret_cast( wordParam ), &clientRect, brush ); + DeleteObject( brush ); + return 1; + } + break; + + case WM_MIRROR_BORDER: + UpdateBorderWindow(); + return 0; + + case WM_TIMER: + if( wordParam == TOPMOST_TIMER_ID ) + { + SetWindowPos( window, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE ); + if( m_backdropWindow != nullptr ) + { + // Keep the backdrop directly below the mirror. + SetWindowPos( m_backdropWindow, window, 0, 0, 0, 0, + SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE ); + } + + // Static zoom and draw cover the source monitor with a + // full-screen topmost window, hiding the green border; reclaim + // it like live zoom does for the webcam preview. Skip during + // live zoom, whose own reclaim timer would fight this one and + // flicker the border. + if( m_annotationQuery == nullptr || + m_annotationQuery() != AnnotationState::AnnotatingLiveZoom ) + { + HWND border = m_borderWindow != nullptr ? m_borderWindow : m_sourceBorderWindow; + if( border != nullptr && IsWindow( border )) + { + SetWindowPos( border, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE ); + } + } + return 0; + } + break; + + case WM_MIRROR_RELAYOUT: + { + // The mirrored window resized; re-fit to its new aspect ratio. + RECT windowRect = ComputeWindowRect(); + SetWindowPos( window, HWND_TOPMOST, windowRect.left, windowRect.top, + windowRect.right - windowRect.left, + windowRect.bottom - windowRect.top, SWP_NOACTIVATE ); + return 0; + } + + case WM_MOUSEACTIVATE: + return MA_NOACTIVATE; + } + return DefWindowProcW( window, message, wordParam, longParam ); +} + +//---------------------------------------------------------------------------- +// +// MirrorWindow::RenderLoop +// +// Render thread: caches arriving capture frames and presents them. The +// timeout keeps zoom/pan animating when the source is static, since +// Windows.Graphics.Capture only delivers frames on change. +// +//---------------------------------------------------------------------------- +void MirrorWindow::RenderLoop() +{ + winrt::init_apartment( winrt::apartment_type::multi_threaded ); + + while( !m_stopEvent.is_signaled() ) + { + auto frame = m_frameWait->TryGetNextFrame( MIRROR_FRAME_TIMEOUT_MS ); + if( m_stopEvent.is_signaled() ) + { + break; + } + + // Stop if the mirrored window went away. + if( m_sourceWindow != nullptr && !IsWindow( m_sourceWindow ) ) + { + PostMessage( m_notifyWindow, WM_USER_MIRROR_STOP, 0, 0 ); + break; + } + + // Track the source window with the border. + if( m_sourceWindow != nullptr && m_borderWindow != nullptr ) + { + RECT windowRect = GetWindowFrameRect( m_sourceWindow ); + if( !IsRectEmpty( &windowRect ) && !EqualRect( &windowRect, &m_borderTarget )) + { + m_borderTarget = windowRect; + PostMessage( m_window, WM_MIRROR_BORDER, 0, 0 ); + } + } + + // Switch between window and monitor capture as ZoomIt annotation + // modes come and go. On a switch, discard any frame from the + // previous capture. + if( m_annotationQuery && UpdateAnnotationState() ) + { + continue; + } + + if( frame ) + { + // A mirrored window can resize; recreate the frame pool, cache + // texture, and swapchain at the new content size and re-fit the + // mirror window to the new aspect ratio. + if( m_sourceWindow != nullptr && !m_monitorOverride && !m_trackWindow && + frame->ContentSize.Width > 0 && frame->ContentSize.Height > 0 && + ( frame->ContentSize.Width != m_poolSize.Width || + frame->ContentSize.Height != m_poolSize.Height )) + { + m_poolSize = frame->ContentSize; + m_bufferWidth = m_poolSize.Width; + m_bufferHeight = m_poolSize.Height; + m_imageWidth = m_bufferWidth; + m_imageHeight = m_bufferHeight; + m_sourceTexture = nullptr; + m_frameWait->RecreateFramePool( m_poolSize ); + if( FAILED( m_swapChain->ResizeBuffers( 2, m_bufferWidth, m_bufferHeight, + DXGI_FORMAT_B8G8R8A8_UNORM, 0 ) ) ) + { + break; + } + PostMessage( m_window, WM_MIRROR_RELAYOUT, 0, 0 ); + + // Wait for a frame at the new size. + continue; + } + + auto frameTexture = GetDXGIInterfaceFromObject( frame->FrameTexture ); + D3D11_TEXTURE2D_DESC textureDesc; + frameTexture->GetDesc( &textureDesc ); + if( m_sourceTexture != nullptr ) + { + // The cached copy must match the frame exactly for CopyResource. + D3D11_TEXTURE2D_DESC cachedDesc; + m_sourceTexture->GetDesc( &cachedDesc ); + if( cachedDesc.Width != textureDesc.Width || cachedDesc.Height != textureDesc.Height ) + { + m_sourceTexture = nullptr; + } + } + if( m_sourceTexture == nullptr ) + { + textureDesc.Usage = D3D11_USAGE_DEFAULT; + textureDesc.BindFlags = 0; + textureDesc.CPUAccessFlags = 0; + textureDesc.MiscFlags = 0; + if( FAILED( m_device->CreateTexture2D( &textureDesc, nullptr, m_sourceTexture.put() ) ) ) + { + break; + } + } + m_context->CopyResource( m_sourceTexture.get(), frameTexture.get() ); + m_contentSize = frame->ContentSize; + } + + if( m_sourceTexture != nullptr ) + { + RenderFrame(); + } + } + + winrt::uninit_apartment(); +} + +//---------------------------------------------------------------------------- +// +// MirrorWindow::RenderFrame +// +// Copies the mirrored region of the cached source frame into the swapchain +// and presents it. +// +//---------------------------------------------------------------------------- +void MirrorWindow::RenderFrame() +{ + D3D11_TEXTURE2D_DESC sourceDesc; + m_sourceTexture->GetDesc( &sourceDesc ); + + // The mirrored region in texture coordinates, clamped to the captured + // content, which changes when a mirrored window resizes. Window and + // monitor-override captures mirror the full captured content; window + // tracking follows the window rect within the monitor capture. + RECT crop = m_textureCrop; + if( m_trackWindow ) + { + RECT windowRect = GetWindowFrameRect( m_sourceWindow ); + if( IsRectEmpty( &windowRect ) ) + { + return; + } + SetRect( &crop, + windowRect.left - m_sourceMonitorRect.left, + windowRect.top - m_sourceMonitorRect.top, + windowRect.right - m_sourceMonitorRect.left, + windowRect.bottom - m_sourceMonitorRect.top ); + } + else if( m_monitorOverride || m_sourceWindow != nullptr ) + { + SetRect( &crop, 0, 0, m_contentSize.Width, m_contentSize.Height ); + } + crop.left = max( crop.left, 0L ); + crop.top = max( crop.top, 0L ); + crop.right = min( crop.right, static_cast( sourceDesc.Width ) ); + crop.bottom = min( crop.bottom, static_cast( sourceDesc.Height ) ); + const int cropWidth = crop.right - crop.left; + const int cropHeight = crop.bottom - crop.top; + if( cropWidth <= 0 || cropHeight <= 0 ) + { + return; + } + + // A tracked window's crop changes as it resizes; re-fit the mirror. + if( m_trackWindow && ( cropWidth != m_imageWidth || cropHeight != m_imageHeight )) + { + m_imageWidth = cropWidth; + m_imageHeight = cropHeight; + PostMessage( m_window, WM_MIRROR_RELAYOUT, 0, 0 ); + } + + const int width = min( cropWidth, m_bufferWidth ); + const int height = min( cropHeight, m_bufferHeight ); + + D3D11_BOX box; + box.left = crop.left; + box.top = crop.top; + box.right = crop.left + width; + box.bottom = crop.top + height; + box.front = 0; + box.back = 1; + + winrt::com_ptr backBuffer; + if( FAILED( m_swapChain->GetBuffer( 0, winrt::guid_of(), backBuffer.put_void() ) ) ) + { + return; + } + m_context->CopySubresourceRegion( backBuffer.get(), 0, 0, 0, 0, m_sourceTexture.get(), 0, &box ); + m_swapChain->SetSourceSize( width, height ); + m_swapChain->Present( 1, 0 ); +} diff --git a/src/modules/ZoomIt/ZoomIt/MirrorWindow.h b/src/modules/ZoomIt/ZoomIt/MirrorWindow.h new file mode 100644 index 000000000000..2de30c6391cb --- /dev/null +++ b/src/modules/ZoomIt/ZoomIt/MirrorWindow.h @@ -0,0 +1,111 @@ +//============================================================================== +// +// Zoomit +// Sysinternals - www.sysinternals.com +// +// DemoMirror: mirrors a screen region or window, including the mouse cursor, +// onto a second monitor so an audience can follow a demo app on the +// presentation monitor without the presenter leaving the presentation. +// +//============================================================================== +#pragma once + +#include "pch.h" +#include "CaptureFrameWait.h" + +// Posted to the notify window when mirroring must stop because the mirrored +// window closed. Keep distinct from the WM_USER messages in zoomit.h. +#define WM_USER_MIRROR_STOP WM_USER+112 + +// Bright green, distinguishing the mirror border from the yellow +// record/panorama borders and the orange recording-active border. +#define MIRROR_BORDER_COLOR RGB( 0, 255, 0 ) + +class MirrorWindow +{ +public: + // ZoomIt annotation modes (zoom/draw/live zoom) render in overlay + // windows a window capture can't see; while one is active a window + // mirror temporarily switches to capturing the monitor. + enum class AnnotationState { None, Annotating, AnnotatingLiveZoom }; + + ~MirrorWindow() { Stop(); } + + bool IsActive() const { return m_window != nullptr; } + + // Visible window bounds, excluding the invisible resize/shadow frame + // that GetWindowRect includes. + static RECT GetWindowFrameRect( HWND window ); + + // For window mirroring, sourceWindow is the mirrored window and + // sourceRect is its window rect; for region mirroring, sourceWindow is + // null and sourceRect is the mirrored region in screen coordinates. + // With trackWindow, item must be a monitor capture: the mirror shows + // the region under the tracked window instead of the window's own + // surface, so ZoomIt annotations show in place, at the cost of + // occluding windows becoming visible. + // sourceBorderWindow is the caller-owned green border (the region/ + // monitor SelectRectangle); the topmost timer keeps it above the + // full-screen static zoom/draw window. + bool Start( winrt::GraphicsCaptureItem const& item, + RECT sourceRect, + HWND sourceWindow, + HMONITOR sourceMonitor, + HMONITOR targetMonitor, + HWND notifyWindow, + std::function annotationQuery = nullptr, + bool trackWindow = false, + HWND sourceBorderWindow = nullptr ); + void Stop(); + +private: + static const UINT_PTR TOPMOST_TIMER_ID = 1; + // Private to the mirror window: re-fit it to the source's aspect ratio. + static const UINT WM_MIRROR_RELAYOUT = WM_USER + 1; + // Private to the mirror window: move the border to the source window. + static const UINT WM_MIRROR_BORDER = WM_USER + 2; + + void RenderLoop(); + void RenderFrame(); + bool UpdateAnnotationState(); + bool SwitchCapture( winrt::GraphicsCaptureItem const& item, bool enableCursor ); + RECT ComputeWindowRect() const; + void UpdateBorderWindow(); + LRESULT WindowProc( HWND window, UINT message, WPARAM wordParam, LPARAM longParam ); + + const wchar_t* m_className = L"ZoomitMirrorWindow"; + HWND m_window = nullptr; + HWND m_backdropWindow = nullptr; + HWND m_borderWindow = nullptr; + HWND m_notifyWindow = nullptr; + HWND m_sourceWindow = nullptr; + HWND m_sourceBorderWindow = nullptr; // caller-owned region/monitor border + RECT m_borderTarget{}; // source window rect the border follows + RECT m_sourceRect{}; // mirrored region in screen coordinates + RECT m_textureCrop{}; // mirrored region in capture-texture coordinates + RECT m_targetRect{}; // target monitor rectangle + RECT m_sourceMonitorRect{}; // source monitor rectangle (window tracking) + HMONITOR m_sourceMonitor = nullptr; + HMONITOR m_targetMonitor = nullptr; + bool m_trackWindow = false; // monitor capture cropped to the window rect + int m_bufferWidth = 0; // swapchain dimensions + int m_bufferHeight = 0; + int m_imageWidth = 0; // displayed image dimensions, for layout + int m_imageHeight = 0; + winrt::SizeInt32 m_contentSize{}; + winrt::SizeInt32 m_poolSize{}; + + winrt::com_ptr m_device; + winrt::com_ptr m_context; + winrt::com_ptr m_swapChain; + winrt::com_ptr m_sourceTexture; + winrt::Direct3D11::IDirect3DDevice m_winrtDevice{ nullptr }; + std::unique_ptr m_frameWait; + + std::function m_annotationQuery; + AnnotationState m_annotationState = AnnotationState::None; + bool m_monitorOverride = false; // capturing the monitor while annotating + + std::thread m_renderThread; + wil::unique_event m_stopEvent{ wil::EventOptions::ManualReset }; +}; diff --git a/src/modules/ZoomIt/ZoomIt/SelectRectangle.cpp b/src/modules/ZoomIt/ZoomIt/SelectRectangle.cpp index 680cd1db1449..9886906961e0 100644 --- a/src/modules/ZoomIt/ZoomIt/SelectRectangle.cpp +++ b/src/modules/ZoomIt/ZoomIt/SelectRectangle.cpp @@ -36,7 +36,7 @@ static void SelectRectangleDebugLog( const wchar_t* format, ... ) bool SelectRectangle::Start( HWND ownerWindow, bool fullMonitor ) { m_stopping = false; - m_borderColor = RGB( 255, 222, 0 ); // Reset to yellow; turns orange once first frame is captured. + m_borderColor = m_configuredBorderColor; // Reset from the recording-active orange. m_recordingActive = false; // Reset so SetRecordingActive fires on next recording. m_fullMonitor = fullMonitor; diff --git a/src/modules/ZoomIt/ZoomIt/SelectRectangle.h b/src/modules/ZoomIt/ZoomIt/SelectRectangle.h index 83d6b25d7c7e..c213c9db1295 100644 --- a/src/modules/ZoomIt/ZoomIt/SelectRectangle.h +++ b/src/modules/ZoomIt/ZoomIt/SelectRectangle.h @@ -21,6 +21,8 @@ class SelectRectangle int MinSize() const { return m_minSize; } void AspectRatio( double ratio ) { m_aspectRatio = ratio; } double AspectRatio() const { return m_aspectRatio; } + // Sets the border color Start resets to (default: capture-API yellow). + void BorderColor( COLORREF color ) { m_configuredBorderColor = color; m_borderColor = color; } RECT SelectedRect() const { return m_selectedRect; } bool IsActive() const { return m_window != nullptr; } @@ -46,6 +48,7 @@ class SelectRectangle double m_aspectRatio = 0.0; // 0 = no constraint, e.g. 16.0/9.0 RECT m_selectedRect{}; COLORREF m_borderColor = RGB( 255, 222, 0 ); // default: yellow (matches capture API) + COLORREF m_configuredBorderColor = RGB( 255, 222, 0 ); // color Start resets to bool m_recordingActive = false; // true once first frame is captured bool m_fullMonitor = false; // true when recording full screen diff --git a/src/modules/ZoomIt/ZoomIt/ZoomIt.rc b/src/modules/ZoomIt/ZoomIt/ZoomIt.rc index 1d9b63f0c434..6b8b343663ae 100644 --- a/src/modules/ZoomIt/ZoomIt/ZoomIt.rc +++ b/src/modules/ZoomIt/ZoomIt/ZoomIt.rc @@ -122,7 +122,7 @@ BEGIN DEFPUSHBUTTON "OK",IDOK,184,308,50,14 PUSHBUTTON "Cancel",IDCANCEL,241,308,50,14 LTEXT "ZoomIt v12.11",IDC_VERSION,42,7,73,10 - LTEXT "Copyright © 2006-2026 Mark Russinovich",IDC_COPYRIGHT,42,17,251,8 + LTEXT "Copyright � 2006-2026 Mark Russinovich",IDC_COPYRIGHT,42,17,251,8 CONTROL "Sysinternals - www.sysinternals.com",IDC_LINK, "SysLink",WS_TABSTOP,42,26,150,9 ICON "APPICON",IDC_STATIC,12,9,20,20 @@ -261,6 +261,18 @@ BEGIN CONTROL "",IDC_LIVE_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,73,85,80,12 END +DEMOMIRROR DIALOGEX 0, 0, 317, 136 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "DemoMirror mirrors the screen, a region of the screen, or a window, including the mouse pointer, onto a second monitor. Use it to show a demo app on the presentation monitor, on top of the slide show, without leaving the presentation.",IDC_STATIC,7,7,272,27 + LTEXT "Enter the hotkey to mirror the entire screen, enter it with the Shift key to select a region to mirror, or with the Alt key to mirror the window under the cursor. Enter the hotkey again to stop mirroring.",IDC_STATIC,7,39,272,27 + LTEXT "Mirror Toggle:",IDC_STATIC,7,73,62,8 + CONTROL "",IDC_MIRROR_HOTKEY,"msctls_hotkey32",WS_BORDER | WS_TABSTOP,73,71,80,12 + LTEXT "When mirroring a window, DemoMirror can track the window's screen region so ZoomIt zoom and draw show in place. Uncheck to mirror the window's surface directly, unaffected by overlapping windows.",IDC_STATIC,7,91,272,27 + CONTROL "Track window region:",IDC_MIRROR_TRACK_WINDOW,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | BS_RIGHT | WS_TABSTOP,7,121,90,10 +END + RECORD DIALOGEX 0, 0, 276, 228 STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU FONT 8, "MS Shell Dlg", 400, 0, 0x1 @@ -450,6 +462,14 @@ BEGIN BOTTOMMARGIN, 89 END + "DEMOMIRROR", DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 279 + TOPMARGIN, 7 + BOTTOMMARGIN, 97 + END + "RECORD", DIALOG BEGIN LEFTMARGIN, 7 @@ -521,6 +541,11 @@ BEGIN 0 END +DEMOMIRROR AFX_DIALOG_LAYOUT +BEGIN + 0 +END + DRAW AFX_DIALOG_LAYOUT BEGIN 0 diff --git a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj index 2aadd244e7fb..52f7ee8533f2 100644 --- a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj +++ b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj @@ -257,6 +257,7 @@ NotUsing + Use @@ -478,6 +479,7 @@ + diff --git a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj.filters b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj.filters index b881496177b8..21869b44ac99 100644 --- a/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj.filters +++ b/src/modules/ZoomIt/ZoomIt/ZoomIt.vcxproj.filters @@ -30,6 +30,9 @@ Source Files + + Source Files + Source Files @@ -131,6 +134,9 @@ Header Files + + Header Files + Header Files diff --git a/src/modules/ZoomIt/ZoomIt/ZoomItSettings.h b/src/modules/ZoomIt/ZoomIt/ZoomItSettings.h index f28001dbf49f..84be96a1c742 100644 --- a/src/modules/ZoomIt/ZoomIt/ZoomItSettings.h +++ b/src/modules/ZoomIt/ZoomIt/ZoomItSettings.h @@ -21,6 +21,8 @@ DWORD g_SnipSaveToggleKey = ((HOTKEYF_CONTROL | HOTKEYF_SHIFT) << 8) | '6'; DWORD g_SnipPanoramaToggleKey = ((HOTKEYF_CONTROL) << 8) | '8'; DWORD g_SnipPanoramaSaveToggleKey = ((HOTKEYF_CONTROL | HOTKEYF_SHIFT) << 8) | '8'; DWORD g_SnipOcrToggleKey = ((HOTKEYF_CONTROL | HOTKEYF_ALT) << 8) | '6'; +DWORD g_MirrorToggleKey = ((HOTKEYF_CONTROL) << 8) | '9'; +BOOLEAN g_MirrorTrackWindow = TRUE; DWORD g_ShowExpiredTime = 1; DWORD g_SliderZoomLevel = 3; @@ -86,6 +88,8 @@ REG_SETTING RegSettings[] = { { L"SnipPanoramaToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipPanoramaToggleKey, static_cast(g_SnipPanoramaToggleKey) }, { L"SnipPanoramaSaveToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipPanoramaSaveToggleKey, static_cast(g_SnipPanoramaSaveToggleKey) }, { L"SnipOcrToggleKey", SETTING_TYPE_DWORD, 0, &g_SnipOcrToggleKey, static_cast(g_SnipOcrToggleKey) }, + { L"MirrorToggleKey", SETTING_TYPE_DWORD, 0, &g_MirrorToggleKey, static_cast(g_MirrorToggleKey) }, + { L"MirrorTrackWindow", SETTING_TYPE_BOOLEAN, 0, &g_MirrorTrackWindow, static_cast(g_MirrorTrackWindow) }, { L"PenColor", SETTING_TYPE_DWORD, 0, &g_PenColor, static_cast(g_PenColor) }, { L"PenWidth", SETTING_TYPE_DWORD, 0, &g_RootPenWidth, static_cast(g_RootPenWidth) }, { L"OptionsShown", SETTING_TYPE_BOOLEAN, 0, &g_OptionsShown, static_cast(g_OptionsShown) }, diff --git a/src/modules/ZoomIt/ZoomIt/Zoomit.cpp b/src/modules/ZoomIt/ZoomIt/Zoomit.cpp index a4d1795fe4d4..10e4d90b8b15 100644 --- a/src/modules/ZoomIt/ZoomIt/Zoomit.cpp +++ b/src/modules/ZoomIt/ZoomIt/Zoomit.cpp @@ -17,6 +17,7 @@ #include "ZoomItSettings.h" #include "GifRecordingSession.h" #include "WebcamPreviewWindow.h" +#include "MirrorWindow.h" #include "BreakTimer.h" #include "PanoramaCapture.h" #include "ImageEncoder.h" @@ -102,6 +103,9 @@ COLORREF g_CustomColors[16]; #define SNIP_PANORAMA_HOTKEY 19 #define SNIP_PANORAMA_SAVE_HOTKEY 20 #define WEBCAM_TOGGLE_HOTKEY 21 +#define MIRROR_HOTKEY 22 +#define MIRROR_WINDOW_HOTKEY 23 +#define MIRROR_CROP_HOTKEY 24 #define ZOOM_PAGE 0 #define LIVE_PAGE 1 @@ -117,6 +121,7 @@ COLORREF g_CustomColors[16]; #define RECORD_PAGE 6 #define SNIP_PAGE 7 #define PANORAMA_PAGE 8 +#define MIRROR_PAGE 9 OPTION_TABS g_OptionsTabs[] = { { _T("Zoom"), NULL }, @@ -127,7 +132,8 @@ OPTION_TABS g_OptionsTabs[] = { { _T("Break"), NULL }, { _T("Record"), NULL }, { _T("Snip"), NULL }, - { _T("Panorama"), NULL } + { _T("Panorama"), NULL }, + { _T("DemoMirror"), NULL } }; static const TCHAR* g_RecordingFormats[] = { @@ -176,6 +182,7 @@ DWORD g_SnipPanoramaToggleMod; DWORD g_SnipOcrToggleMod; DWORD g_SnipSaveToggleMod; DWORD g_SnipPanoramaSaveToggleMod; +DWORD g_MirrorToggleMod; BOOLEAN g_ZoomOnLiveZoom = FALSE; DWORD g_PenWidth = PEN_WIDTH; @@ -214,6 +221,8 @@ BOOL g_RecordToggle = FALSE; BOOL g_RecordCropping = FALSE; SelectRectangle g_SelectRectangle; WebcamPreviewWindow g_WebcamPreview; +SelectRectangle g_MirrorSelectRectangle; +MirrorWindow g_MirrorWindow; // The full path of the last saved recording file. std::wstring g_RecordingSaveLocation; // The last user-chosen recording filename. Used to construct unique recording filenames. @@ -418,6 +427,9 @@ const wchar_t* HotkeyIdToString( WPARAM hotkeyId ) case SNIP_OCR_HOTKEY: return L"SNIP_OCR_HOTKEY"; case SNIP_PANORAMA_HOTKEY: return L"SNIP_PANORAMA_HOTKEY"; case SNIP_PANORAMA_SAVE_HOTKEY: return L"SNIP_PANORAMA_SAVE_HOTKEY"; + case MIRROR_HOTKEY: return L"MIRROR_HOTKEY"; + case MIRROR_WINDOW_HOTKEY: return L"MIRROR_WINDOW_HOTKEY"; + case MIRROR_CROP_HOTKEY: return L"MIRROR_CROP_HOTKEY"; default: return L"UNKNOWN_HOTKEY"; } } @@ -3559,6 +3571,9 @@ void UnregisterAllHotkeys( HWND hWnd ) unregisterHotkey( SAVE_CROP_HOTKEY ); unregisterHotkey( COPY_IMAGE_HOTKEY ); unregisterHotkey( COPY_CROP_HOTKEY ); + unregisterHotkey( MIRROR_HOTKEY ); + unregisterHotkey( MIRROR_WINDOW_HOTKEY ); + unregisterHotkey( MIRROR_CROP_HOTKEY ); } //---------------------------------------------------------------------------- @@ -3612,6 +3627,17 @@ void RegisterAllHotkeys(HWND hWnd) registerHotkey( RECORD_WINDOW_HOTKEY, windowMod | MOD_NOREPEAT, g_RecordToggleKey & 0xFF ); } } + if (g_MirrorToggleKey) { + registerHotkey( MIRROR_HOTKEY, g_MirrorToggleMod | MOD_NOREPEAT, g_MirrorToggleKey & 0xFF ); + UINT mirrorCropMod = g_MirrorToggleMod ^ MOD_SHIFT; + UINT mirrorWindowMod = g_MirrorToggleMod ^ MOD_ALT; + if ( mirrorCropMod != 0 ) { + registerHotkey( MIRROR_CROP_HOTKEY, mirrorCropMod | MOD_NOREPEAT, g_MirrorToggleKey & 0xFF ); + } + if ( mirrorWindowMod != 0 ) { + registerHotkey( MIRROR_WINDOW_HOTKEY, mirrorWindowMod | MOD_NOREPEAT, g_MirrorToggleKey & 0xFF ); + } + } // Note: COPY_IMAGE_HOTKEY, COPY_CROP_HOTKEY (Ctrl+C, Ctrl+Shift+C) and // SAVE_IMAGE_HOTKEY, SAVE_CROP_HOTKEY (Ctrl+S, Ctrl+Shift+S) are registered @@ -4828,6 +4854,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, DWORD newSnipSaveToggleKey, newSnipSaveToggleMod; DWORD newSnipPanoramaSaveToggleKey, newSnipPanoramaSaveToggleMod; DWORD newLiveZoomToggleKey, newLiveZoomToggleMod; + DWORD newMirrorToggleKey, newMirrorToggleMod; static std::vector> microphones; auto CleanupFonts = [&]() @@ -5065,6 +5092,9 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, if( g_SnipPanoramaToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_HOTKEY), HKM_SETHOTKEY, g_SnipPanoramaToggleKey, 0 ); if( g_SnipPanoramaSaveToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_SAVE_HOTKEY), HKM_SETHOTKEY, g_SnipPanoramaSaveToggleKey, 0 ); if( g_SnipOcrToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_OCR_HOTKEY), HKM_SETHOTKEY, g_SnipOcrToggleKey, 0 ); + if( g_MirrorToggleKey) SendMessage( GetDlgItem( g_OptionsTabs[MIRROR_PAGE].hPage, IDC_MIRROR_HOTKEY), HKM_SETHOTKEY, g_MirrorToggleKey, 0 ); + CheckDlgButton( g_OptionsTabs[MIRROR_PAGE].hPage, IDC_MIRROR_TRACK_WINDOW, + g_MirrorTrackWindow ? BST_CHECKED: BST_UNCHECKED ); CheckDlgButton( hDlg, IDC_SHOW_TRAY_ICON, g_ShowTrayIcon ? BST_CHECKED: BST_UNCHECKED ); CheckDlgButton( hDlg, IDC_AUTOSTART, @@ -5529,6 +5559,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, newSnipPanoramaToggleKey = static_cast(SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_HOTKEY), HKM_GETHOTKEY, 0, 0 )); newSnipPanoramaSaveToggleKey = static_cast(SendMessage( GetDlgItem( g_OptionsTabs[PANORAMA_PAGE].hPage, IDC_SNIP_PANORAMA_SAVE_HOTKEY), HKM_GETHOTKEY, 0, 0 )); newSnipOcrToggleKey = static_cast(SendMessage( GetDlgItem( g_OptionsTabs[SNIP_PAGE].hPage, IDC_SNIP_OCR_HOTKEY), HKM_GETHOTKEY, 0, 0 )); + newMirrorToggleKey = static_cast(SendMessage( GetDlgItem( g_OptionsTabs[MIRROR_PAGE].hPage, IDC_MIRROR_HOTKEY), HKM_GETHOTKEY, 0, 0 )); newToggleMod = GetKeyMod( newToggleKey ); newLiveZoomToggleMod = GetKeyMod( newLiveZoomToggleKey ); @@ -5541,6 +5572,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, newSnipPanoramaToggleMod = GetKeyMod( newSnipPanoramaToggleKey ); newSnipPanoramaSaveToggleMod = GetKeyMod( newSnipPanoramaSaveToggleKey ); newSnipOcrToggleMod = GetKeyMod( newSnipOcrToggleKey ); + newMirrorToggleMod = GetKeyMod( newMirrorToggleKey ); g_SliderZoomLevel = static_cast(SendMessage( GetDlgItem(g_OptionsTabs[ZOOM_PAGE].hPage, IDC_ZOOM_SLIDER), TBM_GETPOS, 0, 0 )); g_DemoTypeSpeedSlider = static_cast(SendMessage( GetDlgItem( g_OptionsTabs[DEMOTYPE_PAGE].hPage, IDC_DEMOTYPE_SPEED_SLIDER ), TBM_GETPOS, 0, 0 )); @@ -5552,6 +5584,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, g_MicMonoMix = IsDlgButtonChecked(g_OptionsTabs[RECORD_PAGE].hPage, IDC_MIC_MONO_MIX) == BST_CHECKED; g_NoiseCancellation = IsDlgButtonChecked(g_OptionsTabs[RECORD_PAGE].hPage, IDC_NOISE_CANCELLATION) == BST_CHECKED; g_RecordAspectRatio = IsDlgButtonChecked(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_ASPECT_RATIO) == BST_CHECKED; + g_MirrorTrackWindow = IsDlgButtonChecked(g_OptionsTabs[MIRROR_PAGE].hPage, IDC_MIRROR_TRACK_WINDOW) == BST_CHECKED; GetDlgItemText( g_OptionsTabs[BREAK_PAGE].hPage, IDC_TIMER, text, 3 ); text[2] = 0; newTimeout = _tstoi( text ); @@ -5662,6 +5695,16 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, APPNAME, MB_ICONERROR); UnregisterAllHotkeys(GetParent(hDlg)); break; + } + else if( UINT mirrorCropMod = newMirrorToggleMod ^ MOD_SHIFT, mirrorWindowMod = newMirrorToggleMod ^ MOD_ALT; newMirrorToggleKey && + (!RegisterHotKey(GetParent(hDlg), MIRROR_HOTKEY, newMirrorToggleMod | MOD_NOREPEAT, newMirrorToggleKey & 0xFF) || + (mirrorCropMod != 0 && !RegisterHotKey(GetParent(hDlg), MIRROR_CROP_HOTKEY, mirrorCropMod | MOD_NOREPEAT, newMirrorToggleKey & 0xFF)) || + (mirrorWindowMod != 0 && !RegisterHotKey(GetParent(hDlg), MIRROR_WINDOW_HOTKEY, mirrorWindowMod | MOD_NOREPEAT, newMirrorToggleKey & 0xFF)))) { + + MessageBox(hDlg, L"The specified mirror hotkey is already in use.\nSelect a different mirror hotkey.", + APPNAME, MB_ICONERROR); + UnregisterAllHotkeys(GetParent(hDlg)); + break; } else { g_BreakTimeout = newTimeout; @@ -5686,6 +5729,8 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message, g_SnipPanoramaSaveToggleMod = newSnipPanoramaSaveToggleMod; g_SnipOcrToggleKey = newSnipOcrToggleKey; g_SnipOcrToggleMod = newSnipOcrToggleMod; + g_MirrorToggleKey = newMirrorToggleKey; + g_MirrorToggleMod = newMirrorToggleMod; reg.WriteRegSettings( RegSettings ); EnableDisableTrayIcon( GetParent( hDlg ), g_ShowTrayIcon ); @@ -6958,6 +7003,45 @@ auto GetUniqueScreenshotFilename() return std::wstring(buffer); } +//---------------------------------------------------------------------------- +// +// FindMirrorTargetMonitor +// +// Picks the monitor to mirror onto: the one showing a PowerPoint slide show +// if there is one, otherwise the first monitor that isn't the source. +// +//---------------------------------------------------------------------------- +HMONITOR FindMirrorTargetMonitor( HMONITOR sourceMonitor ) +{ + HWND hWndSlideShow = FindWindow( L"screenClass", NULL ); + if( hWndSlideShow != NULL && IsWindowVisible( hWndSlideShow ) ) { + + HMONITOR hMonitor = MonitorFromWindow( hWndSlideShow, MONITOR_DEFAULTTONEAREST ); + if( hMonitor != sourceMonitor ) { + + return hMonitor; + } + } + + struct MONITOR_SEARCH { + HMONITOR sourceMonitor; + HMONITOR targetMonitor; + } search = { sourceMonitor, NULL }; + + EnumDisplayMonitors( NULL, NULL, []( HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam ) -> BOOL { + + auto search = reinterpret_cast( lParam ); + if( hMonitor != search->sourceMonitor && search->targetMonitor == NULL ) { + + search->targetMonitor = hMonitor; + return FALSE; + } + return TRUE; + }, reinterpret_cast( &search )); + + return search.targetMonitor; +} + //---------------------------------------------------------------------------- // // StartRecordingAsync @@ -7754,6 +7838,7 @@ LRESULT APIENTRY MainWndProc( g_SnipPanoramaSaveToggleMod = GetKeyMod( g_SnipPanoramaSaveToggleKey ); g_SnipOcrToggleMod = GetKeyMod( g_SnipOcrToggleKey ); g_RecordToggleMod = GetKeyMod( g_RecordToggleKey ); + g_MirrorToggleMod = GetKeyMod( g_MirrorToggleKey ); if( !g_OptionsShown && !g_StartedByPowerToys ) { // First run should show options when running as standalone. If not running as standalone, @@ -7854,6 +7939,18 @@ LRESULT APIENTRY MainWndProc( showOptions = TRUE; } } + if (showOptions == FALSE && g_MirrorToggleKey) { + UINT mirrorCropMod = g_MirrorToggleMod ^ MOD_SHIFT; + UINT mirrorWindowMod = g_MirrorToggleMod ^ MOD_ALT; + if (!RegisterHotKey(hWnd, MIRROR_HOTKEY, g_MirrorToggleMod | MOD_NOREPEAT, g_MirrorToggleKey & 0xFF) || + (mirrorCropMod != 0 && !RegisterHotKey(hWnd, MIRROR_CROP_HOTKEY, mirrorCropMod | MOD_NOREPEAT, g_MirrorToggleKey & 0xFF)) || + (mirrorWindowMod != 0 && !RegisterHotKey(hWnd, MIRROR_WINDOW_HOTKEY, mirrorWindowMod | MOD_NOREPEAT, g_MirrorToggleKey & 0xFF))) { + + MessageBox(hWnd, L"The specified mirror hotkey is already in use.\nSelect a different mirror hotkey.", + APPNAME, MB_ICONERROR); + showOptions = TRUE; + } + } if( showOptions ) { SendMessage( hWnd, WM_COMMAND, IDC_OPTIONS, 0 ); @@ -8612,6 +8709,147 @@ LRESULT APIENTRY MainWndProc( } break; + case MIRROR_HOTKEY: + case MIRROR_CROP_HOTKEY: + case MIRROR_WINDOW_HOTKEY: { + + // + // DemoMirror: mirror the screen, a region (Shift), or a window + // (Alt), including the mouse cursor, onto a second monitor on + // top of a slide show so the audience can follow a demo without + // the presenter leaving the presentation. Entered once to start + // mirroring and again to stop. + // + if( g_MirrorWindow.IsActive() ) { + + g_MirrorWindow.Stop(); + g_MirrorSelectRectangle.Stop(); + break; + } + + if( g_RecordCropping == TRUE || g_bSaveInProgress ) { + + break; + } + + bool mirrorCaptureSupported = false; + try { + + mirrorCaptureSupported = winrt::GraphicsCaptureSession::IsSupported(); + } + catch( const winrt::hresult_error& ) {} + if( !mirrorCaptureSupported ) { + + MessageBox( hWnd, L"Screen mirroring requires Windows 10, May 2019 Update or higher.", APPNAME, MB_OK ); + break; + } + + HWND hWndMirrorSource = NULL; + HMONITOR mirrorSourceMonitor = NULL; + RECT mirrorSourceRect = {}; + + if( wParam == MIRROR_WINDOW_HOTKEY ) { + + // Mirror the top-level window under the cursor. + POINT mirrorPoint; + GetCursorPos( &mirrorPoint ); + hWndMirrorSource = WindowFromPoint( mirrorPoint ); + while( hWndMirrorSource != NULL && GetParent( hWndMirrorSource ) != NULL ) { + + hWndMirrorSource = GetParent( hWndMirrorSource ); + } + if( hWndMirrorSource == NULL || hWndMirrorSource == GetDesktopWindow() || + hWndMirrorSource == hWnd ) { + + break; + } + mirrorSourceMonitor = MonitorFromWindow( hWndMirrorSource, MONITOR_DEFAULTTONEAREST ); + mirrorSourceRect = MirrorWindow::GetWindowFrameRect( hWndMirrorSource ); + + } else if( wParam == MIRROR_CROP_HOTKEY ) { + + // Select the region to mirror. The selection border stays up + // to show what's being mirrored; it is excluded from capture. + g_RecordCropping = TRUE; + g_MirrorSelectRectangle.BorderColor( MIRROR_BORDER_COLOR ); + g_MirrorSelectRectangle.AspectRatio( 0.0 ); + bool mirrorCanceled = !g_MirrorSelectRectangle.Start( nullptr ); + g_RecordCropping = FALSE; + if( mirrorCanceled ) { + + break; + } + mirrorSourceRect = g_MirrorSelectRectangle.SelectedRect(); + mirrorSourceMonitor = MonitorFromRect( &mirrorSourceRect, MONITOR_DEFAULTTONEAREST ); + + // The selection border defaults to translucent; make the + // mirror border fully opaque so it reads bright green. + SetLayeredWindowAttributes( g_MirrorSelectRectangle.Window(), 0, 255, LWA_ALPHA ); + + } else { + + // Mirror the entire monitor under the cursor, with a + // full-monitor border showing that mirroring is active. + POINT mirrorPoint; + GetCursorPos( &mirrorPoint ); + mirrorSourceMonitor = MonitorFromPoint( mirrorPoint, MONITOR_DEFAULTTONEAREST ); + MONITORINFO mirrorMonitorInfo = { sizeof( mirrorMonitorInfo ) }; + GetMonitorInfo( mirrorSourceMonitor, &mirrorMonitorInfo ); + mirrorSourceRect = mirrorMonitorInfo.rcMonitor; + + g_MirrorSelectRectangle.BorderColor( MIRROR_BORDER_COLOR ); + g_MirrorSelectRectangle.AspectRatio( 0.0 ); + g_MirrorSelectRectangle.Start( nullptr, true ); + SetLayeredWindowAttributes( g_MirrorSelectRectangle.Window(), 0, 255, LWA_ALPHA ); + } + + HMONITOR mirrorTargetMonitor = FindMirrorTargetMonitor( mirrorSourceMonitor ); + if( mirrorTargetMonitor == NULL ) { + + g_MirrorSelectRectangle.Stop(); + MessageBox( hWnd, L"Screen mirroring requires a second monitor.", APPNAME, MB_OK ); + break; + } + + // With window tracking, mirror the monitor region under the + // window instead of the window's own surface so ZoomIt zoom and + // draw annotations show in place. + bool mirrorTrackWindow = ( hWndMirrorSource != NULL && g_MirrorTrackWindow ); + + winrt::Windows::Graphics::Capture::GraphicsCaptureItem mirrorItem{ nullptr }; + try { + + if( hWndMirrorSource != NULL && !mirrorTrackWindow ) + mirrorItem = util::CreateCaptureItemForWindow( hWndMirrorSource ); + else + mirrorItem = util::CreateCaptureItemForMonitor( mirrorSourceMonitor ); + } + catch( const winrt::hresult_error& ) {} + + // Reports when a zoom/draw/live-zoom mode is active so a window + // mirror can temporarily capture the monitor instead - those + // modes render in overlay windows a window capture can't see. + auto mirrorAnnotationQuery = []() { + if( g_hWndLiveZoom != NULL && IsWindowVisible( g_hWndLiveZoom )) + return MirrorWindow::AnnotationState::AnnotatingLiveZoom; + if( g_Zoomed ) + return MirrorWindow::AnnotationState::Annotating; + return MirrorWindow::AnnotationState::None; + }; + + HWND mirrorBorderWindow = hWndMirrorSource == NULL ? g_MirrorSelectRectangle.Window() : NULL; + if( mirrorItem == nullptr || + !g_MirrorWindow.Start( mirrorItem, mirrorSourceRect, hWndMirrorSource, + mirrorSourceMonitor, mirrorTargetMonitor, hWnd, + mirrorAnnotationQuery, mirrorTrackWindow, + mirrorBorderWindow )) { + + g_MirrorSelectRectangle.Stop(); + MessageBox( hWnd, L"Unable to start screen mirroring.", APPNAME, MB_OK ); + } + break; + } + case ZOOM_HOTKEY: // // Zoom @@ -10295,6 +10533,12 @@ LRESULT APIENTRY MainWndProc( StopRecording(); break; + case WM_USER_MIRROR_STOP: + // The mirrored window closed. + g_MirrorWindow.Stop(); + g_MirrorSelectRectangle.Stop(); + break; + case WM_USER_RECORDING_STARTED: // The first video frame has been captured. Change the selection // border from yellow to orange so the user knows recording is live. @@ -10424,6 +10668,7 @@ LRESULT APIENTRY MainWndProc( g_SnipPanoramaSaveToggleMod = GetKeyMod(g_SnipPanoramaSaveToggleKey); g_SnipOcrToggleMod = GetKeyMod(g_SnipOcrToggleKey); g_RecordToggleMod = GetKeyMod(g_RecordToggleKey); + g_MirrorToggleMod = GetKeyMod(g_MirrorToggleKey); BOOL showOptions = FALSE; if (g_ToggleKey) { @@ -10553,6 +10798,21 @@ LRESULT APIENTRY MainWndProc( showOptions = TRUE; } } + if (g_MirrorToggleKey) + { + UINT mirrorCropMod = g_MirrorToggleMod ^ MOD_SHIFT; + UINT mirrorWindowMod = g_MirrorToggleMod ^ MOD_ALT; + if (!RegisterHotKey(hWnd, MIRROR_HOTKEY, g_MirrorToggleMod | MOD_NOREPEAT, g_MirrorToggleKey & 0xFF) || + (mirrorCropMod != 0 && !RegisterHotKey(hWnd, MIRROR_CROP_HOTKEY, mirrorCropMod | MOD_NOREPEAT, g_MirrorToggleKey & 0xFF)) || + (mirrorWindowMod != 0 && !RegisterHotKey(hWnd, MIRROR_WINDOW_HOTKEY, mirrorWindowMod | MOD_NOREPEAT, g_MirrorToggleKey & 0xFF))) + { + if(!g_StartedByPowerToys) + { + MessageBox(hWnd, L"The specified mirror hotkey is already in use.\nSelect a different mirror hotkey.", APPNAME, MB_ICONERROR); + } + showOptions = TRUE; + } + } if (showOptions) { // To open the PowerToys settings in the ZoomIt page. diff --git a/src/modules/ZoomIt/ZoomIt/resource.h b/src/modules/ZoomIt/ZoomIt/resource.h index 7651e66dad60..c313b5aee699 100644 --- a/src/modules/ZoomIt/ZoomIt/resource.h +++ b/src/modules/ZoomIt/ZoomIt/resource.h @@ -139,6 +139,8 @@ #define IDC_NOISE_CANCELLATION 1133 #define IDC_SNIP_SAVE_HOTKEY 1134 #define IDC_SNIP_PANORAMA_SAVE_HOTKEY 1135 +#define IDC_MIRROR_HOTKEY 1136 +#define IDC_MIRROR_TRACK_WINDOW 1137 #define IDC_SAVE 40002 #define IDC_COPY 40004 #define IDC_RECORD 40006 @@ -154,7 +156,7 @@ #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 120 #define _APS_NEXT_COMMAND_VALUE 40012 -#define _APS_NEXT_CONTROL_VALUE 1136 +#define _APS_NEXT_CONTROL_VALUE 1138 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif diff --git a/src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettings.cpp b/src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettings.cpp index e24c03e3c332..108350432fae 100644 --- a/src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettings.cpp +++ b/src/modules/ZoomIt/ZoomItSettingsInterop/ZoomItSettings.cpp @@ -76,6 +76,7 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation { L"SnipPanoramaSaveToggleKey", SPECIAL_SEMANTICS_SHORTCUT }, { L"BreakTimerKey", SPECIAL_SEMANTICS_SHORTCUT }, { L"DemoTypeToggleKey", SPECIAL_SEMANTICS_SHORTCUT }, + { L"MirrorToggleKey", SPECIAL_SEMANTICS_SHORTCUT }, { L"PenColor", SPECIAL_SEMANTICS_COLOR }, { L"BreakPenColor", SPECIAL_SEMANTICS_COLOR }, { L"Font", SPECIAL_SEMANTICS_LOG_FONT }, diff --git a/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs b/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs index 0ea5635a093b..78ff7b96315c 100644 --- a/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs +++ b/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs @@ -46,6 +46,9 @@ public ZoomItProperties() [CmdConfigureIgnore] public static HotkeySettings DefaultDemoTypeToggleKey => new HotkeySettings(false, true, false, false, '7'); // Ctrl+7 + [CmdConfigureIgnore] + public static HotkeySettings DefaultMirrorToggleKey => new HotkeySettings(false, true, false, false, '9'); // Ctrl+9 + public KeyboardKeysProperty ToggleKey { get; set; } public KeyboardKeysProperty LiveZoomToggleKey { get; set; } @@ -138,5 +141,9 @@ public ZoomItProperties() public BoolProperty RecordAspectRatio { get; set; } public BoolProperty BreakLockWorkstation { get; set; } + + public KeyboardKeysProperty MirrorToggleKey { get; set; } + + public BoolProperty MirrorTrackWindow { get; set; } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ZoomItPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/ZoomItPage.xaml index 3f6fc2abd598..13baefc117ef 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ZoomItPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ZoomItPage.xaml @@ -399,6 +399,29 @@ + + + + + + + + + + + + + + + + + + + Press **{0}** to record a specific window + + DemoMirror + + + Mirror the screen, a region of the screen, or a window, including the mouse pointer, onto a second monitor, on top of a slide show. Use ZoomIt zoom and draw to annotate the mirrored image. + + + Mirror activation + + + Press **{0}** to start or stop mirroring the screen + + + Press **{0}** to mirror a portion of the screen + + + Press **{0}** to mirror a specific window + + + Mirror windows by tracking their screen region, so zoom and draw show in place + Scaling diff --git a/src/settings-ui/Settings.UI/ViewModels/ZoomItViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/ZoomItViewModel.cs index 322fd6e9ae4b..7a4020745b17 100644 --- a/src/settings-ui/Settings.UI/ViewModels/ZoomItViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/ZoomItViewModel.cs @@ -407,6 +407,74 @@ public HotkeySettings RecordToggleKeyWindow } } + public HotkeySettings MirrorToggleKey + { + get => _zoomItSettings.Properties.MirrorToggleKey.Value; + set + { + if (_zoomItSettings.Properties.MirrorToggleKey.Value != value) + { + _zoomItSettings.Properties.MirrorToggleKey.Value = value ?? ZoomItProperties.DefaultMirrorToggleKey; + OnPropertyChanged(nameof(MirrorToggleKey)); + OnPropertyChanged(nameof(MirrorToggleKeyCrop)); + OnPropertyChanged(nameof(MirrorToggleKeyWindow)); + NotifySettingsChanged(); + } + } + } + + public HotkeySettings MirrorToggleKeyCrop + { + get + { + var baseKey = _zoomItSettings.Properties.MirrorToggleKey.Value; + if (baseKey == null) + { + return null; + } + + // XOR with Shift: if Shift is present, remove it; if absent, add it. + // If the result would have no modifier keys, return null to avoid displaying a bare-key shortcut label. + if (baseKey.Shift && !baseKey.Win && !baseKey.Ctrl && !baseKey.Alt) + { + return null; + } + + return new HotkeySettings( + baseKey.Win, + baseKey.Ctrl, + baseKey.Alt, + !baseKey.Shift, // XOR with Shift + baseKey.Code); + } + } + + public HotkeySettings MirrorToggleKeyWindow + { + get + { + var baseKey = _zoomItSettings.Properties.MirrorToggleKey.Value; + if (baseKey == null) + { + return null; + } + + // XOR with Alt: if Alt is present, remove it; if absent, add it. + // If the result would have no modifier keys, return null to avoid displaying a bare-key shortcut label. + if (baseKey.Alt && !baseKey.Win && !baseKey.Ctrl && !baseKey.Shift) + { + return null; + } + + return new HotkeySettings( + baseKey.Win, + baseKey.Ctrl, + !baseKey.Alt, // XOR with Alt + baseKey.Shift, + baseKey.Code); + } + } + public HotkeySettings SnipToggleKey { get => _zoomItSettings.Properties.SnipToggleKey.Value; @@ -1189,6 +1257,20 @@ public bool RecordAspectRatio } } + public bool MirrorTrackWindow + { + get => _zoomItSettings.Properties.MirrorTrackWindow.Value; + set + { + if (_zoomItSettings.Properties.MirrorTrackWindow.Value != value) + { + _zoomItSettings.Properties.MirrorTrackWindow.Value = value; + OnPropertyChanged(nameof(MirrorTrackWindow)); + NotifySettingsChanged(); + } + } + } + private void NotifySettingsChanged() { global::PowerToys.ZoomItSettingsInterop.ZoomItSettings.SaveSettingsJson(