fix(winstate): apply the wanted window state inline, killing the restore flash

The MINIMIZEEND correction was posted through ForceQueue. Instrumenting the
hook showed the window state identical at the hook and one message-loop turn
later, so the queue guarded no race and only bought the normal-sized frame the
user saw. ApplyWantedWindowState now runs inline; the deferred pass stays as an
idempotent net.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-27 19:39:23 +01:00
parent 99b68c3e67
commit b52cdc2d0b
2 changed files with 37 additions and 26 deletions
+9 -8
View File
@@ -47,10 +47,10 @@ Préparer : 3 entrées `A`, `B`, `C`, dont `A` dans le dossier `Work` avec le ta
- **ACTION** : `A` ouverte + dirty → glisser la ligne `A` sur `Perso` dans la sidebar → Save - **ACTION** : `A` ouverte + dirty → glisser la ligne `A` sur `Perso` dans la sidebar → Save
- **ATTENDU** : identique à A3.✅ - **ATTENDU** : identique à A3.✅
### A6 · glisser une **multi-sélection** sur un dossier (`moveEntriesToFolder`) ⚠️ ### A6 · glisser une **multi-sélection** sur un dossier (`moveEntriesToFolder`) ✅
- **ACTION** : cocher `A` + `B` → glisser sur `Perso` → recharger (F5 impossible : relancer l'app) - **ACTION** : cocher `A` + `B` → glisser sur `Perso` → recharger (F5 impossible : relancer l'app)
- **ATTENDU** : dossier changé **et** nom / username / site / tags **intacts**. - **ATTENDU** : dossier changé **et** nom / username / site / tags **intacts**.
- **Historique** : ce chemin n'appelait pas `withEncryptedMeta` → il blanchissait les métadonnées chiffrées. Si des entrées ont été déplacées ainsi avant le fix, elles sont peut-être déjà vides — vérifier.✅ - **Historique** : ce chemin n'appelait pas `withEncryptedMeta` → il blanchissait les métadonnées chiffrées. Si des entrées ont été déplacées ainsi avant le fix, elles sont peut-être déjà vides — vérifier.
### A7 · suppression du dossier de l'entrée éditée ### A7 · suppression du dossier de l'entrée éditée
- **ACTION** : `A` (dossier `Work`) ouverte + dirty → sidebar → supprimer le dossier `Work` → Save - **ACTION** : `A` (dossier `Work`) ouverte + dirty → sidebar → supprimer le dossier `Work` → Save
@@ -337,22 +337,23 @@ L'app démarre maximisée (`WindowState = wsMaximized` dans la `.fmx`).
### I1 · maximisé survit à Show desktop ✅ ### I1 · maximisé survit à Show desktop ✅
- **ACTION** : fenêtre **maximisée** → `Win+D` (ou *Show desktop*) → revenir à l'app (barre des tâches, tray ou Alt+Tab). - **ACTION** : fenêtre **maximisée** → `Win+D` (ou *Show desktop*) → revenir à l'app (barre des tâches, tray ou Alt+Tab).
- **ATTENDU** : elle revient **maximisée**. - **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** : 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 correct, il restait un scintillement — la fenêtre revenait normale puis se maximisait. La correction était postée via `ForceQueue` ; le log a montré que l'état ne changeait **pas** entre le hook et le tour de boucle suivant, donc la file ne protégeait d'aucune course et n'ajoutait que la frame visible. Elle s'exécute maintenant **en ligne** dans `MINIMIZEEND` (`ApplyWantedWindowState`), la passe différée restant en filet idempotent. Piste écartée au passage : poser `WPF_RESTORETOMAXIMIZED` — `GetWindowPlacement` rapporte déjà ce flag sur toute fenêtre maximisée, donc le stamp est un no-op.
### I2 · normal survit aussi ✅ ### I2 · normal survit aussi ✅
- **ACTION** : dé-maximiser la fenêtre → `Win+D` → revenir. - **ACTION** : dé-maximiser la fenêtre → `Win+D` → revenir.
- **ATTENDU** : elle reste en **taille normale** — le correctif ne doit jamais forcer le maximise. - **ATTENDU** : elle reste en **taille normale** — le correctif ne doit jamais forcer le maximise.
### I3 · maximiser / dé-maximiser reste enregistré ### I3 · maximiser / dé-maximiser reste enregistré ✅
- **ACTION** : dé-maximiser → `Win+D` → retour (normal) → maximiser → `Win+D` → retour. - **ACTION** : dé-maximiser → `Win+D` → retour (normal) → maximiser → `Win+D` → retour.
- **ATTENDU** : chaque retour respecte le **dernier geste** de l'utilisateur. - **ATTENDU** : chaque retour respecte le **dernier geste** de l'utilisateur.
### I4 · retour depuis le tray ### I4 · retour depuis le tray ✅
- **ACTION** : fenêtre maximisée → fermer (part au tray) → clic sur l'icône du tray. - **ACTION** : fenêtre maximisée → fermer (part au tray) → clic sur l'icône du tray.
- **ATTENDU** : revient maximisée (chemin distinct : `RestoreFromTray` rejoue son propre placement). - **ATTENDU** : revient maximisée (chemin distinct : `RestoreFromTray` rejoue son propre placement).
### I5 · le garde de minimisation ne reste pas bloqué (autofill → tray) ⚠️ ### I5 · le garde de minimisation ne reste pas bloqué (autofill → tray) ✅
Le chemin le plus subtil : `ExecuteAutofill` **minimise réellement** la fenêtre Le chemin le plus subtil : `ExecuteAutofill` **minimise réellement** la fenêtre
(`SW_MINIMIZE`) avant de taper, ce qui lève le garde ; si elle est ensuite (`SW_MINIMIZE`) avant de taper, ce qui lève le garde ; si elle est ensuite
**cachée** dans le tray au lieu d'être restaurée, `MINIMIZEEND` peut ne jamais **cachée** dans le tray au lieu d'être restaurée, `MINIMIZEEND` peut ne jamais
@@ -450,7 +451,7 @@ Le flag `-tray` est ce que « Start with Windows » écrit dans `HKCU\…\Run`
dossier de sortie. **Ne pas** laisser `-tray` dans *Run → Parameters* de l'IDE, dossier de sortie. **Ne pas** laisser `-tray` dans *Run → Parameters* de l'IDE,
sinon chaque F9 démarre caché. sinon chaque F9 démarre caché.
### L1 · démarrage caché, sans bouton de barre des tâches ### L1 · démarrage caché, sans bouton de barre des tâches ✅
- **ACTION** : lancer avec `-tray` (l'app n'étant pas déjà lancée — sinon le - **ACTION** : lancer avec `-tray` (l'app n'étant pas déjà lancée — sinon le
mutex single-instance fait sortir la nouvelle instance immédiatement). mutex single-instance fait sortir la nouvelle instance immédiatement).
- **ATTENDU** : **aucune fenêtre**, **aucun bouton** dans la barre des tâches, - **ATTENDU** : **aucune fenêtre**, **aucun bouton** dans la barre des tâches,
@@ -462,7 +463,7 @@ sinon chaque F9 démarre caché.
non plus : il faut retirer `WS_EX_APPWINDOW` et poser `WS_EX_TOOLWINDOW`, non plus : il faut retirer `WS_EX_APPWINDOW` et poser `WS_EX_TOOLWINDOW`,
depuis le `.dpr`, juste après `Application.Initialize`. depuis le `.dpr`, juste après `Application.Initialize`.
### L2 · le bouton revient à la restauration ### L2 · le bouton revient à la restauration ✅
- **ACTION** : après L1, clic sur l'icône du tray. - **ACTION** : après L1, clic sur l'icône du tray.
- **ATTENDU** : la fenêtre s'ouvre **et** le bouton réapparaît dans la barre - **ATTENDU** : la fenêtre s'ouvre **et** le bouton réapparaît dans la barre
des tâches. des tâches.
+18 -8
View File
@@ -176,6 +176,7 @@ type
procedure RestoreWindowFromTray; procedure RestoreWindowFromTray;
procedure WinStateMinimizeStart; procedure WinStateMinimizeStart;
procedure WinStateMinimizeEnd; procedure WinStateMinimizeEnd;
procedure ApplyWantedWindowState;
end; end;
var var
@@ -1657,18 +1658,31 @@ begin
FIgnoreResizes := True; FIgnoreResizes := True;
end; end;
// Back up: re-apply what the user had. Queued so it lands after FMX has // Back up: re-apply what the user had.
// finished sizing the window — inline loses that race.
procedure TMainForm.WinStateMinimizeEnd; procedure TMainForm.WinStateMinimizeEnd;
begin begin
// 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;
// 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, TThread.ForceQueue(nil,
procedure procedure
begin
ApplyWantedWindowState;
// Release the guard only after our own ShowWindow's resize has passed.
TThread.ForceQueue(nil, procedure begin FIgnoreResizes := False; end);
end);
end;
procedure TMainForm.ApplyWantedWindowState;
var var
LHwnd: HWND; LHwnd: HWND;
begin begin
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd; LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
if (LHwnd <> 0) and (not IsIconic(LHwnd)) then if (LHwnd = 0) or IsIconic(LHwnd) then Exit;
begin
if FWantMaximized and (not IsZoomed(LHwnd)) then if FWantMaximized and (not IsZoomed(LHwnd)) then
begin begin
LogLine('[winstate] re-maximising after restore'); LogLine('[winstate] re-maximising after restore');
@@ -1680,10 +1694,6 @@ begin
ShowWindow(LHwnd, SW_RESTORE); ShowWindow(LHwnd, SW_RESTORE);
end; end;
end; end;
// Release the guard only after our own ShowWindow's resize has passed.
TThread.ForceQueue(nil, procedure begin FIgnoreResizes := False; end);
end);
end;
// Track the state the user settles on. Ignored entirely while a minimise / // Track the state the user settles on. Ignored entirely while a minimise /
// restore is in flight — that is the whole point of the hook above. // restore is in flight — that is the whole point of the hook above.