From 03c638b2e1ed5b22ba35182b31de55cfb5093230 Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:22:09 +0100 Subject: [PATCH] fix(window): ask Windows whether a resize is part of a restore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- delphi-backend/UMainForm.pas | 122 +++++++++++++++-------------------- 1 file changed, 52 insertions(+), 70 deletions(-) diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index 53ed562..fc4d6a8 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -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 + if IsIconic(LHwnd) then Exit; // nothing to learn while minimised + + // 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 - 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 + // Re-apply after FMX has finished sizing; doing it inline loses the race. + if FWantMaximized and (not IsZoomed(LHwnd)) and (not FRestoring) then 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); + FRestoring := True; + 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; + LogLine('[winstate] re-maximising after restore'); + ShowWindow(LH, SW_SHOWMAXIMIZED); + end); + end; + Exit; // never record a restore as intent + end; + if FRestoring then Exit; // our own ShowWindow's resize + FWantMaximized := IsZoomed(LHwnd); // genuine user gesture end; -// 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. +// 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 - LWant: Boolean; + LHwnd: HWND; 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 + 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: 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. + LogLine('[winstate] activate: re-maximising (want=True, zoomed=False)'); 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); - end; - FWantMaximized := LWant; // our own ShowWindow fired a resize too + FRestoring := False; + LH := WindowHandleToPlatform(Self.Handle).Wnd; + if (LH = 0) or IsIconic(LH) or IsZoomed(LH) then Exit; + ShowWindow(LH, SW_SHOWMAXIMIZED); end); end;