diff --git a/TEST_SCENARIOS.md b/TEST_SCENARIOS.md index f293288..7d573f2 100644 --- a/TEST_SCENARIOS.md +++ b/TEST_SCENARIOS.md @@ -185,7 +185,8 @@ plus bas. - **Deuxième cause, côté Delphi** : `Ctrl+Shift+Q` et `Ctrl+Shift+A` appellent `RestoreWindowFromTray` **avant** que JS ne puisse refuser — un garde JS arrive trop tard par construction. Restaurer la fenêtre propriétaire d'une modale native laisse le dialogue en rade derrière tout le reste : l'app semble gelée alors qu'elle est simplement *désactivée* par une boîte devenue introuvable (au point qu'il fallait un utilitaire de capture d'écran pour la faire réapparaître). - **Garde** : `NativeModalActive` teste `IsWindowEnabled` sur notre fenêtre — Windows la désactive tant qu'une modale native lui appartient. Posé en tête des **quatre** points d'entrée hotkey (quick search hotkey + menu tray, new entry, autofill). Il ne se contente pas de refuser : il **ramène le dialogue au premier plan** via `GetLastActivePopup`, ce qui est précisément ce dont l'utilisateur a besoin. - **ATTENDU après correctif** : pendant l'« Enregistrer sous », n'importe quel hotkey **fait réapparaître le dialogue au premier plan, avec le focus**, et une **notification tray** explique que les raccourcis restent inactifs tant qu'il n'a pas répondu. -- **Deux pièges du message et du focus** : `SetForegroundWindow` seul est **refusé** — le hotkey vient d'une autre application, donc nous ne sommes pas le processus de premier plan ; il faut `ForceForegroundWindow` (`AttachThreadInput`), désormais exposé par `PM.Bridge`. Et le message passe par une **balloon tray**, pas par un toast : notre fenêtre est désactivée et derrière le dialogue, rien de rendu dans la page ne serait visible. +- **Piège du focus** : `SetForegroundWindow` est **refusé** à un processus qui n'est pas déjà devant, et le hotkey vient d'une autre application. Il faut attacher notre file d'entrées au thread de la **fenêtre de premier plan** (VS Code). Réutiliser le helper d'autofill ne marche **pas** : il attache au thread de la *cible*, or le dialogue appartient à notre propre thread — l'attache est un no-op et le dialogue remontait sans jamais recevoir le focus. +- **Piège du message** : une **balloon tray**, pas un toast — notre fenêtre est désactivée et derrière le dialogue, rien de rendu dans la page ne serait visible. Elle suit le réglage « alertes tray » : désactivé, tu auras le focus sans le message. ### B14 · dialogues natifs ouverts par un clic programmatique - **ACTION** : `A` ouverte + **dirty** → (a) Settings → *Import vault* ; (b) télécharger une pièce jointe (fallback web) ; (c) *Upload icon* ; (d) joindre un fichier. diff --git a/delphi-backend/Source/PM.Bridge.pas b/delphi-backend/Source/PM.Bridge.pas index ea8c7ce..c707b56 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -272,12 +272,6 @@ type read FOnQuickSearchHotkey write FOnQuickSearchHotkey; end; -// Bring a window to the foreground despite the Win10/11 anti-focus-stealing -// rules (AttachThreadInput + poll). Exposed because the host needs it to raise -// a native dialog of ours that got buried — plain SetForegroundWindow is -// refused when we are not the foreground process. -function ForceForegroundWindow(ATargetHwnd: HWND): Boolean; - implementation uses @@ -313,6 +307,7 @@ function UnhookWinEvent(hWinEventHook: THandle): BOOL; // Defined further down but needed by the constructor / MinimizeToTray. function IsOwnProcessWindow(AHwnd: HWND): Boolean; forward; function IsShellWindow(AHwnd: HWND): Boolean; forward; +function ForceForegroundWindow(ATargetHwnd: HWND): Boolean; forward; procedure ForegroundEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND; idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward; diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index 2e1703e..b61dd30 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -514,10 +514,22 @@ begin if (LPopup <> 0) and (LPopup <> LHwnd) and IsWindow(LPopup) then begin ShowWindow(LPopup, SW_SHOW); - // Not plain SetForegroundWindow: the hotkey is pressed from ANOTHER - // application, so we are not the foreground process and Windows refuses - // the call — the dialog was raised but never focused. - ForceForegroundWindow(LPopup); + // Windows refuses SetForegroundWindow to a process that isn't already in + // front, and the hotkey comes from ANOTHER application. The way through is + // to attach our input queue to the CURRENT FOREGROUND thread — not to the + // dialog's, which is our own thread and would make the attach a no-op + // (that is why the autofill helper, written for cross-process targets, + // left the dialog raised but unfocused). + var LFgThread := GetWindowThreadProcessId(GetForegroundWindow, nil); + var LOurThread := GetCurrentThreadId; + var LAttached := (LFgThread <> 0) and (LFgThread <> LOurThread) and + AttachThreadInput(LOurThread, LFgThread, True); + try + BringWindowToTop(LPopup); + SetForegroundWindow(LPopup); + finally + if LAttached then AttachThreadInput(LOurThread, LFgThread, False); + end; // A tray balloon, not a toast: our own window is disabled and sits behind // the dialog, so nothing rendered in the page would be seen. FBridge.ShowBalloon('Finish the open dialog',