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
|
// which happens however the user comes back. FWantMaximized mirrors the
|
||||||
// user's LAST deliberate choice, so we never fight a normal-size window.
|
// user's LAST deliberate choice, so we never fight a normal-size window.
|
||||||
FWantMaximized: Boolean;
|
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
|
// Set from activate until the queued correction runs, so the restore's
|
||||||
// own resize is not mistaken for the user resizing the window.
|
// own resize is not mistaken for the user resizing the window.
|
||||||
FRestoring: Boolean;
|
FRestoring: Boolean;
|
||||||
// A resize awaiting confirmation that no activate follows it.
|
|
||||||
FResizePending, FPendingZoomed: Boolean;
|
|
||||||
FServer: TPMHTTPServer;
|
FServer: TPMHTTPServer;
|
||||||
FBridge: TPMBridge;
|
FBridge: TPMBridge;
|
||||||
// Accumulates base64 chunks for large file saves (attachments too big
|
// 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.
|
// The .fmx ships WindowState = wsMaximized, so that is the starting intent.
|
||||||
// Wired here (not in the .fmx) to keep the designer file untouched.
|
// Wired here (not in the .fmx) to keep the designer file untouched.
|
||||||
FWantMaximized := (WindowState = TWindowState.wsMaximized);
|
FWantMaximized := (WindowState = TWindowState.wsMaximized);
|
||||||
FWasMinimized := False;
|
|
||||||
FRestoring := False;
|
FRestoring := False;
|
||||||
FResizePending := False;
|
|
||||||
OnResize := FormResizeTrackState;
|
OnResize := FormResizeTrackState;
|
||||||
OnActivate := FormActivateRestoreState;
|
OnActivate := FormActivateRestoreState;
|
||||||
|
|
||||||
@@ -1516,86 +1509,75 @@ end;
|
|||||||
|
|
||||||
// Remember the state the USER settled on. Reads Win32 (IsZoomed/IsIconic)
|
// Remember the state the USER settled on. Reads Win32 (IsZoomed/IsIconic)
|
||||||
// rather than FMX's WindowState, which does not reliably follow a native
|
// rather than FMX's WindowState, which does not reliably follow a native
|
||||||
// un-maximise — that is why the first version never cleared the flag.
|
// un-maximise.
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
procedure TMainForm.FormResizeTrackState(Sender: TObject);
|
procedure TMainForm.FormResizeTrackState(Sender: TObject);
|
||||||
var
|
var
|
||||||
LHwnd: HWND;
|
LHwnd: HWND;
|
||||||
|
LPlace: TWindowPlacement;
|
||||||
|
LIsRestore: Boolean;
|
||||||
begin
|
begin
|
||||||
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
||||||
if LHwnd = 0 then Exit;
|
if LHwnd = 0 then Exit;
|
||||||
if IsIconic(LHwnd) then
|
if IsIconic(LHwnd) then Exit; // nothing to learn while minimised
|
||||||
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;
|
|
||||||
|
|
||||||
// Re-apply the remembered state once the window is back. Runs however the
|
// OnActivate turned out to be useless as a "the window came back" signal —
|
||||||
// user returns (taskbar, tray, Alt+Tab) — a plain taskbar restore never goes
|
// it also fires on plain focus churn with the WebView2 child, and even right
|
||||||
// through any of our own restore paths. Corrects BOTH directions: FMX drops
|
// after the user clicks Maximize. Ask Windows instead: WPF_RESTORETOMAXIMIZED
|
||||||
// a maximised window to normal, and Windows can restore a normal window
|
// means "this window is on its way back from minimised and belongs
|
||||||
// maximised.
|
// maximised", which is exactly the resize we must not mistake for the user
|
||||||
procedure TMainForm.FormActivateRestoreState(Sender: TObject);
|
// un-maximising.
|
||||||
var
|
FillChar(LPlace, SizeOf(LPlace), 0);
|
||||||
LWant: Boolean;
|
LPlace.length := SizeOf(LPlace);
|
||||||
begin
|
LIsRestore := GetWindowPlacement(LHwnd, @LPlace) and
|
||||||
// Snapshot the intent NOW: FMX's restore-time resize may land before or
|
(((LPlace.flags and WPF_RESTORETOMAXIMIZED) <> 0) or
|
||||||
// after this activate depending on how the window came back, and if it
|
(LPlace.showCmd = SW_SHOWMINIMIZED));
|
||||||
// lands first it would already have overwritten FWantMaximized.
|
LogLine(Format('[winstate] resize zoomed=%s showCmd=%d flags=%d restore=%s want=%s',
|
||||||
// Cancel a resize that has not committed yet: an activate right after one
|
[BoolToStr(IsZoomed(LHwnd), True), LPlace.showCmd, LPlace.flags,
|
||||||
// means this was a restore, not the user resizing the window.
|
BoolToStr(LIsRestore, True), BoolToStr(FWantMaximized, True)]));
|
||||||
FResizePending := False;
|
|
||||||
LWant := FWantMaximized;
|
if LIsRestore then
|
||||||
if FRestoring then Exit; // one correction per return
|
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;
|
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,
|
TThread.ForceQueue(nil,
|
||||||
procedure
|
procedure
|
||||||
var
|
var LH: HWND;
|
||||||
LHwnd: HWND;
|
|
||||||
begin
|
begin
|
||||||
FRestoring := False;
|
FRestoring := False;
|
||||||
FWasMinimized := False;
|
LH := WindowHandleToPlatform(Self.Handle).Wnd;
|
||||||
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
if (LH = 0) or IsIconic(LH) or IsZoomed(LH) then Exit;
|
||||||
if (LHwnd = 0) or IsIconic(LHwnd) then Exit;
|
LogLine('[winstate] re-maximising after restore');
|
||||||
if LWant <> IsZoomed(LHwnd) then
|
ShowWindow(LH, SW_SHOWMAXIMIZED);
|
||||||
begin
|
end);
|
||||||
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;
|
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);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user