fix(window): ask Windows whether a resize is part of a restore

The traces killed the previous discriminator: OnActivate fires constantly
(WebView2 focus churn) and even right after the user clicks Maximize, so
"resize followed by activate" wrongly classified a user maximise as a restore
and dropped the intent.

Windows itself knows: WPF_RESTORETOMAXIMIZED (or showCmd still
SW_SHOWMINIMIZED) marks the resize of a window on its way back from minimised.
That resize is now never recorded as intent, and triggers the re-maximise
directly — no dependency on activate ordering. Activate is demoted to a
guarded safety net.

Log line now dumps zoomed/showCmd/flags so the next run confirms the flag is
actually set on this machine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-25 20:22:09 +01:00
parent 28f0d1c5bc
commit 03c638b2e1
+56 -74
View File
@@ -79,14 +79,9 @@ type
// which happens however the user comes back. FWantMaximized mirrors the
// user's LAST deliberate choice, so we never fight a normal-size window.
FWantMaximized: Boolean;
// True between an external minimise and the following activate, so the
// restore's own resize is not mistaken for a user resize.
FWasMinimized: Boolean;
// Set from activate until the queued correction runs, so the restore's
// own resize is not mistaken for the user resizing the window.
FRestoring: Boolean;
// A resize awaiting confirmation that no activate follows it.
FResizePending, FPendingZoomed: Boolean;
FServer: TPMHTTPServer;
FBridge: TPMBridge;
// Accumulates base64 chunks for large file saves (attachments too big
@@ -182,9 +177,7 @@ begin
// The .fmx ships WindowState = wsMaximized, so that is the starting intent.
// Wired here (not in the .fmx) to keep the designer file untouched.
FWantMaximized := (WindowState = TWindowState.wsMaximized);
FWasMinimized := False;
FRestoring := False;
FResizePending := False;
OnResize := FormResizeTrackState;
OnActivate := FormActivateRestoreState;
@@ -1516,86 +1509,75 @@ end;
// Remember the state the USER settled on. Reads Win32 (IsZoomed/IsIconic)
// rather than FMX's WindowState, which does not reliably follow a native
// un-maximise — that is why the first version never cleared the flag.
//
// The restore itself fires a resize too, and that one must NOT be recorded:
// it reports whatever FMX decided (often "normal"), which would erase the
// intent we are trying to preserve. FWasMinimized gates it out.
// un-maximise.
procedure TMainForm.FormResizeTrackState(Sender: TObject);
var
LHwnd: HWND;
LPlace: TWindowPlacement;
LIsRestore: Boolean;
begin
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
if LHwnd = 0 then Exit;
if IsIconic(LHwnd) then
begin
FWasMinimized := True; // the state we must survive, not record
LogLine('[winstate] resize: minimised');
Exit;
end;
// Observed order on a restore: FMX fires its resize BEFORE the activate, so
// committing here would destroy the intent we are about to need. A genuine
// user resize is never followed by an activate (the window is already
// active) — so defer the commit one message-pump turn and let an incoming
// activate cancel it.
// Nothing to learn when the maximised state is unchanged — and this is what
// keeps a mouse-drag resize (which fires continuously) from flooding the
// queue and the log.
if IsZoomed(LHwnd) = FWantMaximized then Exit;
FPendingZoomed := IsZoomed(LHwnd);
FResizePending := True;
LogLine('[winstate] resize pending: zoomed=' + BoolToStr(FPendingZoomed, True));
TThread.ForceQueue(nil,
procedure
begin
if not FResizePending then Exit; // an activate claimed it: a restore
FResizePending := False;
FWantMaximized := FPendingZoomed;
LogLine('[winstate] resize committed: want_max=' +
BoolToStr(FWantMaximized, True));
end);
end;
if IsIconic(LHwnd) then Exit; // nothing to learn while minimised
// Re-apply the remembered state once the window is back. Runs however the
// user returns (taskbar, tray, Alt+Tab) — a plain taskbar restore never goes
// through any of our own restore paths. Corrects BOTH directions: FMX drops
// a maximised window to normal, and Windows can restore a normal window
// maximised.
procedure TMainForm.FormActivateRestoreState(Sender: TObject);
var
LWant: Boolean;
begin
// Snapshot the intent NOW: FMX's restore-time resize may land before or
// after this activate depending on how the window came back, and if it
// lands first it would already have overwritten FWantMaximized.
// Cancel a resize that has not committed yet: an activate right after one
// means this was a restore, not the user resizing the window.
FResizePending := False;
LWant := FWantMaximized;
if FRestoring then Exit; // one correction per return
// OnActivate turned out to be useless as a "the window came back" signal —
// it also fires on plain focus churn with the WebView2 child, and even right
// after the user clicks Maximize. Ask Windows instead: WPF_RESTORETOMAXIMIZED
// means "this window is on its way back from minimised and belongs
// maximised", which is exactly the resize we must not mistake for the user
// un-maximising.
FillChar(LPlace, SizeOf(LPlace), 0);
LPlace.length := SizeOf(LPlace);
LIsRestore := GetWindowPlacement(LHwnd, @LPlace) and
(((LPlace.flags and WPF_RESTORETOMAXIMIZED) <> 0) or
(LPlace.showCmd = SW_SHOWMINIMIZED));
LogLine(Format('[winstate] resize zoomed=%s showCmd=%d flags=%d restore=%s want=%s',
[BoolToStr(IsZoomed(LHwnd), True), LPlace.showCmd, LPlace.flags,
BoolToStr(LIsRestore, True), BoolToStr(FWantMaximized, True)]));
if LIsRestore then
begin
// Re-apply after FMX has finished sizing; doing it inline loses the race.
if FWantMaximized and (not IsZoomed(LHwnd)) and (not FRestoring) then
begin
FRestoring := True;
LogLine('[winstate] activate: want_max=' + BoolToStr(LWant, True) +
' was_min=' + BoolToStr(FWasMinimized, True));
// Queue the correction so it runs AFTER FMX has finished restoring the
// window — applying it inline loses the race against FMX's own sizing.
TThread.ForceQueue(nil,
procedure
var
LHwnd: HWND;
var LH: HWND;
begin
FRestoring := False;
FWasMinimized := False;
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
if (LHwnd = 0) or IsIconic(LHwnd) then Exit;
if LWant <> IsZoomed(LHwnd) then
begin
LogLine('[winstate] correcting to ' +
BoolToStr(LWant, True) + ' (was zoomed=' +
BoolToStr(IsZoomed(LHwnd), True) + ')');
if LWant then ShowWindow(LHwnd, SW_SHOWMAXIMIZED)
else ShowWindow(LHwnd, SW_RESTORE);
LH := WindowHandleToPlatform(Self.Handle).Wnd;
if (LH = 0) or IsIconic(LH) or IsZoomed(LH) then Exit;
LogLine('[winstate] re-maximising after restore');
ShowWindow(LH, SW_SHOWMAXIMIZED);
end);
end;
FWantMaximized := LWant; // our own ShowWindow fired a resize too
Exit; // never record a restore as intent
end;
if FRestoring then Exit; // our own ShowWindow's resize
FWantMaximized := IsZoomed(LHwnd); // genuine user gesture
end;
// Safety net only: if the window is back, visible and not maximised while the
// user's last deliberate state WAS maximised, put it right. Guarded so the
// constant activate churn (WebView2 focus) can't act on its own.
procedure TMainForm.FormActivateRestoreState(Sender: TObject);
var
LHwnd: HWND;
begin
if FRestoring or (not FWantMaximized) then Exit;
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
if (LHwnd = 0) or IsIconic(LHwnd) or IsZoomed(LHwnd) then Exit;
FRestoring := True;
LogLine('[winstate] activate: re-maximising (want=True, zoomed=False)');
TThread.ForceQueue(nil,
procedure
var LH: HWND;
begin
FRestoring := False;
LH := WindowHandleToPlatform(Self.Handle).Wnd;
if (LH = 0) or IsIconic(LH) or IsZoomed(LH) then Exit;
ShowWindow(LH, SW_SHOWMAXIMIZED);
end);
end;