Skip to content

Commit c4c447a

Browse files
shanselmanCopilot
andcommitted
Merge PR #15: Add hotkey support for Windows Snap Assist & PowerToys FancyZones
- WindowMonitor detects maximize via keyboard shortcuts (Win+Up, Snap, FancyZones) - Deferred maximize with MoveSizeEnd event + 200ms fallback timer - All pending-maximize state serialized on UI thread (concurrency-safe) - FullScreenManager: untrack after successful restore, ShowWindow(SW_SHOWNORMAL) - Hot-path Trace logging trimmed for production performance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2 parents 8652077 + 2638db9 commit c4c447a

4 files changed

Lines changed: 120 additions & 15 deletions

File tree

src/MaximizeToVirtualDesktop/FullScreenManager.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,7 @@ public void Restore(IntPtr hwnd)
218218

219219
Trace.WriteLine($"FullScreenManager: Restoring window {hwnd} from temp desktop {entry.TempDesktopId}");
220220

221-
// Untrack this window
222-
_tracker.Untrack(hwnd);
221+
// Untrack will be performed after successful restore
223222

224223
var origDesktop = _vds.FindDesktop(entry.OriginalDesktopId);
225224
try
@@ -229,6 +228,8 @@ public void Restore(IntPtr hwnd)
229228
{
230229
var placement = entry.OriginalPlacement;
231230
NativeMethods.SetWindowPlacement(hwnd, ref placement);
231+
// Ensure the window is shown in its normal (not maximized) state
232+
NativeMethods.ShowWindow(hwnd, (int)NativeMethods.SW_SHOWNORMAL);
232233
}
233234

234235
// Move window back to original desktop
@@ -258,6 +259,8 @@ public void Restore(IntPtr hwnd)
258259
_vds.RemoveDesktop(entry.TempDesktop);
259260
Marshal.ReleaseComObject(entry.TempDesktop);
260261
}
262+
263+
_tracker.Untrack(hwnd);
261264

262265
// Set focus on the restored window
263266
if (NativeMethods.IsWindow(hwnd))

src/MaximizeToVirtualDesktop/SettingsDialog.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ public SettingsDialog(AppSettings settings)
6363
y += grpPin.Height + 12;
6464

6565
// Behavior group
66-
var grpBehavior = new GroupBox { Text = "Behavior", Location = new Point(margin, y), Size = new Size(grpW, 80) };
66+
var grpBehavior = new GroupBox { Text = "Behavior", Location = new Point(margin, y), Size = new Size(grpW, 100) };
6767
Controls.Add(grpBehavior);
68+
// Existing checkbox for InvertShiftClick
6869
_chkInvertShiftClick = new CheckBox
6970
{
70-
Text = "Always maximize to virtual desktop on click\r\n" +
71-
"(Shift+Click performs a normal maximize instead)",
71+
Text = "Shift+Click performs a normal maximize instead",
7272
AutoSize = true,
7373
Checked = settings.InvertShiftClick,
74-
Location = new Point(10, 28),
74+
Location = new Point(10, 48),
7575
};
7676
grpBehavior.Controls.Add(_chkInvertShiftClick);
7777
y += grpBehavior.Height + 12;

src/MaximizeToVirtualDesktop/TrayApplication.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public TrayApplication()
4848
_vds = new VirtualDesktopService();
4949
_tracker = new FullScreenTracker();
5050
_manager = new FullScreenManager(_vds, _tracker);
51-
_monitor = new WindowMonitor(_manager, _tracker, this);
51+
_monitor = new WindowMonitor(_manager, _tracker, this, _settings);
5252
_mouseHook = new MaximizeButtonHook(_manager, this, _settings);
5353

5454
// System tray icon

src/MaximizeToVirtualDesktop/WindowMonitor.cs

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Diagnostics;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
24
using MaximizeToVirtualDesktop.Interop;
35

46
namespace 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

Comments
 (0)