11using System . Diagnostics ;
2+ using System . Collections . Generic ;
3+ using System . Threading . Tasks ;
24using MaximizeToVirtualDesktop . Interop ;
35
46namespace MaximizeToVirtualDesktop ;
@@ -12,23 +14,30 @@ internal sealed class WindowMonitor : IDisposable
1214 private readonly FullScreenManager _manager ;
1315 private readonly FullScreenTracker _tracker ;
1416 private readonly Control _syncControl ;
15-
17+ private readonly AppSettings _settings ;
1618 private IntPtr _locationChangeHook ;
1719 private IntPtr _destroyHook ;
1820 private bool _disposed ;
1921
2022 // Must be stored as fields to prevent GC collection of the delegate
2123 private readonly NativeMethods . WinEventProc _locationChangeProc ;
2224 private readonly NativeMethods . WinEventProc _destroyProc ;
25+ private readonly NativeMethods . WinEventProc _moveSizeEndProc ;
26+ private IntPtr _moveSizeEndHook ;
27+ // Track windows that have been maximized but need to wait for resize end
28+ // Access to this set must happen only on the UI thread.
29+ private readonly HashSet < IntPtr > _pendingMaximize = new ( ) ;
2330
24- public WindowMonitor ( FullScreenManager manager , FullScreenTracker tracker , Control syncControl )
31+ public WindowMonitor ( FullScreenManager manager , FullScreenTracker tracker , Control syncControl , AppSettings settings )
2532 {
2633 _manager = manager ;
2734 _tracker = tracker ;
2835 _syncControl = syncControl ;
36+ _settings = settings ;
2937
3038 _locationChangeProc = OnLocationChange ;
3139 _destroyProc = OnDestroy ;
40+ _moveSizeEndProc = OnMoveSizeEnd ;
3241 }
3342
3443 public void Start ( )
@@ -42,6 +51,13 @@ public void Start()
4251 IntPtr . Zero , _locationChangeProc ,
4352 0 , 0 , NativeMethods . WINEVENT_OUTOFCONTEXT ) ;
4453
54+ // EVENT_SYSTEM_MOVESIZEEND fires after a window finishes moving or resizing (including maximize via shortcuts)
55+ _moveSizeEndHook = NativeMethods . SetWinEventHook (
56+ NativeMethods . EVENT_SYSTEM_MOVESIZEEND ,
57+ NativeMethods . EVENT_SYSTEM_MOVESIZEEND ,
58+ IntPtr . Zero , _moveSizeEndProc ,
59+ 0 , 0 , NativeMethods . WINEVENT_OUTOFCONTEXT ) ;
60+
4561 // EVENT_OBJECT_DESTROY fires when a window is closed
4662 _destroyHook = NativeMethods . SetWinEventHook (
4763 NativeMethods . EVENT_OBJECT_DESTROY ,
@@ -64,16 +80,97 @@ private void OnLocationChange(IntPtr hWinEventHook, uint eventType, IntPtr hwnd,
6480 {
6581 // Only care about top-level window changes (OBJID_WINDOW)
6682 if ( idObject != NativeMethods . OBJID_WINDOW || idChild != 0 ) return ;
67- if ( ! _tracker . IsTracked ( hwnd ) ) return ;
6883
69- // Check if window is still maximized
84+ // If the window is already tracked, check if it is being restored (i.e., no longer maximized)
85+ if ( _tracker . IsTracked ( hwnd ) )
86+ {
87+ var placement = NativeMethods . WINDOWPLACEMENT . Default ;
88+ if ( NativeMethods . GetWindowPlacement ( hwnd , ref placement ) )
89+ {
90+ if ( placement . showCmd != NativeMethods . SW_MAXIMIZE )
91+ {
92+ Trace . WriteLine ( $ "WindowMonitor: Tracked window { hwnd } restored via location change.") ;
93+ MarshalToUiThread ( ( ) => _manager . Restore ( hwnd ) ) ;
94+ return ;
95+ }
96+ }
97+ // Still maximized; let MoveSizeEnd handle pending maximize.
98+ return ;
99+ }
100+
101+ // Not tracked yet: check for a new maximize event (including via shortcut)
102+ var newPlacement = NativeMethods . WINDOWPLACEMENT . Default ;
103+ if ( ! NativeMethods . GetWindowPlacement ( hwnd , ref newPlacement ) ) return ;
104+ if ( newPlacement . showCmd != NativeMethods . SW_MAXIMIZE ) return ;
105+
106+ bool shiftHeld = ( NativeMethods . GetAsyncKeyState ( NativeMethods . VK_SHIFT ) & 0x8000 ) != 0 ;
107+ bool triggerVirtualDesktop = _settings . InvertShiftClick ? ! shiftHeld : shiftHeld ;
108+ if ( triggerVirtualDesktop )
109+ {
110+ // Defer maximization until after the resize operation completes
111+ MarshalToUiThread ( ( ) =>
112+ {
113+ if ( _pendingMaximize . Add ( hwnd ) )
114+ {
115+ Trace . WriteLine ( $ "WindowMonitor: Queued maximize for window { hwnd } after resize end.") ;
116+ // Schedule a fallback in case MoveSizeEnd does not fire (e.g., keyboard shortcut)
117+ _ = Task . Run ( async ( ) =>
118+ {
119+ await Task . Delay ( 200 ) ;
120+ // Marshal the check/remove back onto the UI thread
121+ MarshalToUiThread ( ( ) =>
122+ {
123+ if ( _pendingMaximize . Contains ( hwnd ) )
124+ {
125+ _pendingMaximize . Remove ( hwnd ) ;
126+ var placement = NativeMethods . WINDOWPLACEMENT . Default ;
127+ bool isMaximized = NativeMethods . GetWindowPlacement ( hwnd , ref placement ) && placement . showCmd == NativeMethods . SW_MAXIMIZE ;
128+ if ( isMaximized )
129+ {
130+ Trace . WriteLine ( $ "WindowMonitor: Fallback processing for pending maximize window { hwnd } .") ;
131+ _manager . MaximizeToDesktop ( hwnd ) ;
132+ }
133+ else
134+ {
135+ Trace . WriteLine ( $ "WindowMonitor: Fallback detected restore for pending window { hwnd } .") ;
136+ _manager . Restore ( hwnd ) ;
137+ }
138+ }
139+ } ) ;
140+ } ) ;
141+ }
142+ } ) ;
143+ }
144+ }
145+
146+ private async void OnMoveSizeEnd ( IntPtr hWinEventHook , uint eventType , IntPtr hwnd ,
147+ int idObject , int idChild , uint idEventThread , uint dwmsEventTime )
148+ {
149+ // Only care about top-level window changes (OBJID_WINDOW)
150+ if ( idObject != NativeMethods . OBJID_WINDOW || idChild != 0 ) return ;
151+
152+ // If this window was pending maximize, handle it now
153+ bool wasPending = false ;
154+ if ( ! _syncControl . IsDisposed && _syncControl . IsHandleCreated )
155+ {
156+ wasPending = ( bool ) _syncControl . Invoke ( new Func < bool > ( ( ) => _pendingMaximize . Remove ( hwnd ) ) ) ;
157+ }
158+ if ( wasPending )
159+ {
160+ Trace . WriteLine ( $ "WindowMonitor: MoveSizeEnd triggered for pending maximize window { hwnd } .") ;
161+ MarshalToUiThread ( ( ) => _manager . MaximizeToDesktop ( hwnd ) ) ;
162+ return ;
163+ }
164+
165+ // Handle only tracked windows that are being restored (not maximized)
166+ if ( ! _tracker . IsTracked ( hwnd ) ) return ;
70167 var placement = NativeMethods . WINDOWPLACEMENT . Default ;
71168 if ( ! NativeMethods . GetWindowPlacement ( hwnd , ref placement ) ) return ;
72-
73- if ( placement . showCmd != NativeMethods . SW_SHOWMAXIMIZED )
169+ Trace . WriteLine ( $ "WindowMonitor: MoveSizeEnd: tracked window { hwnd } showCmd= { placement . showCmd } ." ) ;
170+ if ( placement . showCmd != NativeMethods . SW_MAXIMIZE )
74171 {
75- // Window was un-maximized — restore it
76- Trace . WriteLine ( $ "WindowMonitor: Tracked window { hwnd } un-maximized, restoring." ) ;
172+ Trace . WriteLine ( $ "WindowMonitor: Tracked window { hwnd } un-maximized via move/size, restoring." ) ;
173+ await Task . Delay ( 100 ) ;
77174 MarshalToUiThread ( ( ) => _manager . Restore ( hwnd ) ) ;
78175 }
79176 }
@@ -117,6 +214,11 @@ public void Dispose()
117214 NativeMethods . UnhookWinEvent ( _destroyHook ) ;
118215 _destroyHook = IntPtr . Zero ;
119216 }
217+ if ( _moveSizeEndHook != IntPtr . Zero )
218+ {
219+ NativeMethods . UnhookWinEvent ( _moveSizeEndHook ) ;
220+ _moveSizeEndHook = IntPtr . Zero ;
221+ }
120222
121223 Trace . WriteLine ( "WindowMonitor: Disposed." ) ;
122224 }
0 commit comments