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:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user