diff --git a/TEST_SCENARIOS.md b/TEST_SCENARIOS.md index 2f4b464..772e934 100644 --- a/TEST_SCENARIOS.md +++ b/TEST_SCENARIOS.md @@ -340,8 +340,8 @@ L'app démarre maximisée (`WindowState = wsMaximized` dans la `.fmx`). - **ATTENDU** : elle revient **maximisée**, **sans passer par une frame en taille normale**. - **Historique** : FMX oublie l'état maximisé quand Windows minimise la fenêtre de l'extérieur, et aucun de nos chemins de restore ne tourne pour un clic sur la barre des tâches. - **Historique (flash)** : une fois l'état final correct, il restait un scintillement — le cadre apparaissait en taille normale puis grandissait. **C'est un bug FMX**, reproduit sur une application FMX vide : FMX perd le maximisé quand Windows minimise le formulaire de l'extérieur. Windows choisit alors `rcNormalPosition` comme cible **avant** d'animer, donc aucune correction réactive ne peut arriver à temps — trois ont été essayées et rejetées par la mesure : `ForceQueue` (un tour de boucle trop tard), appel inline dans `MINIMIZEEND` (le hook est délivré après le `WM_SIZE`), correction dans le `WM_SIZE` lui-même (l'état devient parfait, le flash reste — preuve que le problème n'était plus l'état). Écarté aussi : poser `WPF_RESTORETOMAXIMIZED`, que `GetWindowPlacement` rapporte déjà à 2 sur une fenêtre maximisée (no-op). - **Solution** : ne pas combattre la restauration, rendre sa cible inoffensive. `WinStateMinimizeStart` gare le vrai `rcNormalPosition` et le remplace par `rcWork` du moniteur ; FMX restaure toujours « en normal », mais normal vaut désormais plein écran, donc rien ne grandit. `ReleaseNormalRect` rend le rectangle réel une fois la fenêtre re-maximisée (invisible : elle est maximisée à ce moment). -- **Non-régression obligatoire** : après le retour, **dé-maximiser** doit rendre la taille d'origine de l'utilisateur, pas la zone de travail — sinon `ReleaseNormalRect` n'a pas tourné et le rectangle est resté garé. + **Statut : non corrigé, assumé.** Une cinquième piste a été tentée puis **annulée** : garer `rcNormalPosition` et le remplacer par `rcWork` du moniteur pendant que la fenêtre est en bas, pour que « restaurer en normal » vaille déjà plein écran. Ça supprimait bien le flash, mais tout `SetWindowPlacement` sur une fenêtre en cours de minimisation — ou déjà iconique — laissait **un moignon de fenêtre dans un coin de l'écran**, y compris en différant l'écriture jusqu'à `IsIconic`. Un artefact visible permanent est pire que l'animation. Reverté. + **Ce qui reste** : l'état final est correct (la fenêtre revient bien maximisée), seule l'animation de restauration est laide. Ne pas rouvrir sans une vraie raison — la seule route restante est de sous-classer le `WndProc` de la fenêtre FMX pour corriger le rectangle dans `WM_WINDOWPOSCHANGING`, ce qui insère du code dans la boucle de messages d'une fenêtre qu'on ne contrôle pas, avec le tray, l'autofill et le démarrage `-tray` en aval. ### I2 · normal survit aussi ✅ - **ACTION** : dé-maximiser la fenêtre → `Win+D` → revenir. diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index c598f51..fed16c0 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -19,7 +19,7 @@ uses System.SysUtils, System.Classes, System.UITypes, System.NetEncoding, System.StrUtils, System.Generics.Collections, System.IOUtils, System.JSON, System.Net.HttpClient, System.Net.URLClient, - Winapi.Windows, Winapi.ShellAPI, Winapi.MultiMon, + Winapi.Windows, Winapi.ShellAPI, FMX.Forms, FMX.Controls, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types, FMX.ScrollBox, FMX.Edit, FMX.Layouts, FMX.Types, FMX.Dialogs, FMX.DialogService, @@ -87,13 +87,6 @@ type // True from "minimise starts" until just after the restore settles, so the // resizes FMX fires while restoring are not recorded as user intent. FIgnoreResizes: Boolean; - // Re-entrance guard: ApplyWantedWindowState runs from OnResize and its own - // ShowWindow fires another OnResize. - FApplyingWindowState: Boolean; - // The window's real restored-size rectangle, parked while we temporarily - // widen it to the monitor's work area (see WinStateMinimizeStart). - FSavedNormalRect: TRect; - FNormalRectHijacked: 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; @@ -184,7 +177,6 @@ type procedure WinStateMinimizeStart; procedure WinStateMinimizeEnd; procedure ApplyWantedWindowState; - procedure ReleaseNormalRect; end; var @@ -1661,70 +1653,21 @@ end; // The window is going down: from here until the restore settles, every resize // FMX fires is part of that transition, never a user gesture. -// FMX loses the maximised state whenever Windows minimises the form from the -// outside — reproducible in an empty FMX app, so there is nothing to fix on our -// side of it. The window comes back at its RESTORED size and DWM animates the -// frame growing towards that rectangle, which is the flash; every correction we -// can run (OnResize, MINIMIZEEND) lands after that decision is made. -// -// So instead of fighting the restore, make the rectangle it restores to -// harmless: park the real one and widen it to the monitor's work area while the -// window is down. FMX still restores "to normal", except normal is now -// full-screen — nothing visibly grows — and the OnResize correction promotes it -// to a true maximised window right after. ApplyWantedWindowState puts the real -// rectangle back, so un-maximising later still lands on the user's own size. procedure TMainForm.WinStateMinimizeStart; -var - LHwnd: HWND; - LPlace: TWindowPlacement; - LMonitor: TMonitorInfo; begin FIgnoreResizes := True; - if FNormalRectHijacked or (not FWantMaximized) then Exit; - LHwnd := WindowHandleToPlatform(Self.Handle).Wnd; - if LHwnd = 0 then Exit; - LPlace := Default(TWindowPlacement); - LPlace.length := SizeOf(LPlace); - if not GetWindowPlacement(LHwnd, @LPlace) then Exit; - LMonitor := Default(TMonitorInfo); - LMonitor.cbSize := SizeOf(LMonitor); - if not GetMonitorInfo(MonitorFromWindow(LHwnd, MONITOR_DEFAULTTONEAREST), @LMonitor) then Exit; - FSavedNormalRect := LPlace.rcNormalPosition; - FNormalRectHijacked := True; - LPlace.rcNormalPosition := LMonitor.rcWork; - // The window is maximised right now, so touching only rcNormalPosition moves - // nothing on screen. - SetWindowPlacement(LHwnd, @LPlace); -end; - -// Undo the hijack above. Safe to call whenever: it is a no-op unless the real -// rectangle is currently parked. -procedure TMainForm.ReleaseNormalRect; -var - LHwnd: HWND; - LPlace: TWindowPlacement; -begin - if not FNormalRectHijacked then Exit; - FNormalRectHijacked := False; - LHwnd := WindowHandleToPlatform(Self.Handle).Wnd; - if LHwnd = 0 then Exit; - LPlace := Default(TWindowPlacement); - LPlace.length := SizeOf(LPlace); - if not GetWindowPlacement(LHwnd, @LPlace) then Exit; - LPlace.rcNormalPosition := FSavedNormalRect; - SetWindowPlacement(LHwnd, @LPlace); end; // Back up: re-apply what the user had. procedure TMainForm.WinStateMinimizeEnd; begin - // Normally a no-op by now: the correction already ran from the restore's own - // OnResize, which Windows delivers BEFORE this hook. Kept for the paths that - // reach here without a resize. + // Inline, not queued: by the time this hook fires Windows has already put the + // window back at its normal size, and measurement showed nothing touches the + // state afterwards — the extra message-loop turn a ForceQueue costs was the + // whole flicker, one painted normal-sized frame before the correction landed. ApplyWantedWindowState; - // Net: whatever path we took, the real restored-size rectangle must never - // stay parked past the end of a restore. - ReleaseNormalRect; + // Second pass anyway, in case FMX sizes the window after us on some path. + // Idempotent: it only acts when the state still disagrees. TThread.ForceQueue(nil, procedure begin @@ -1734,35 +1677,21 @@ begin end); end; -// Re-entrant by nature: it is called from OnResize and its own ShowWindow fires -// another OnResize. FApplyingWindowState keeps that one pass deep. procedure TMainForm.ApplyWantedWindowState; var LHwnd: HWND; begin - if FApplyingWindowState then Exit; LHwnd := WindowHandleToPlatform(Self.Handle).Wnd; - // Iconic means the window is genuinely down (minimise-to-tray, autofill): - // nothing to correct, and forcing a state here would fight the hide. if (LHwnd = 0) or IsIconic(LHwnd) then Exit; - if FWantMaximized = IsZoomed(LHwnd) then Exit; - FApplyingWindowState := True; - try - if FWantMaximized then - begin - LogLine('[winstate] re-maximising after restore'); - ShowWindow(LHwnd, SW_SHOWMAXIMIZED); - // Maximised again, so writing rcNormalPosition back is invisible here — - // and it has to happen before the user can un-maximise. - ReleaseNormalRect; - end - else - begin - LogLine('[winstate] restoring to normal after restore'); - ShowWindow(LHwnd, SW_RESTORE); - end; - finally - FApplyingWindowState := False; + if FWantMaximized and (not IsZoomed(LHwnd)) then + begin + LogLine('[winstate] re-maximising after restore'); + ShowWindow(LHwnd, SW_SHOWMAXIMIZED); + end + else if (not FWantMaximized) and IsZoomed(LHwnd) then + begin + LogLine('[winstate] restoring to normal after restore'); + ShowWindow(LHwnd, SW_RESTORE); end; end; @@ -1772,15 +1701,7 @@ procedure TMainForm.FormResizeTrackState(Sender: TObject); var LHwnd: HWND; begin - // A minimise / restore is in flight, so this resize is never a user gesture — - // but it IS where Windows applies the normal-sized restore, one message before - // MINIMIZEEND is delivered. Correcting from the hook therefore always paints a - // normal frame first; this is the earliest point we get. - if FIgnoreResizes then - begin - ApplyWantedWindowState; - Exit; - end; + if FIgnoreResizes then Exit; LHwnd := WindowHandleToPlatform(Self.Handle).Wnd; if (LHwnd = 0) or IsIconic(LHwnd) then Exit; if FWantMaximized <> IsZoomed(LHwnd) then