From c9c1e98470fa5e8d52db877f61e1a8a43798152f Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sat, 1 Aug 2026 18:49:03 +0100 Subject: [PATCH] fix(hotkeys): focus the buried dialog and say why the shortcut did nothing SetForegroundWindow is refused when the hotkey comes from another application, so the dialog was raised but never focused; ForceForegroundWindow is now exposed by PM.Bridge for that. The explanation goes through a tray balloon because our window is disabled and behind the dialog, where no toast would be seen. --- TEST_SCENARIOS.md | 3 ++- delphi-backend/Source/PM.Bridge.pas | 7 ++++++- delphi-backend/UMainForm.pas | 10 +++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/TEST_SCENARIOS.md b/TEST_SCENARIOS.md index b22110d..f293288 100644 --- a/TEST_SCENARIOS.md +++ b/TEST_SCENARIOS.md @@ -184,7 +184,8 @@ plus bas. - **Généralisation** : un refus ne doit jamais déplacer une fenêtre. Tout nouveau garde qui annule une action doit passer par ce chemin. - **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 lieu de l'enterrer. +- **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. ### 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 c707b56..ea8c7ce 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -272,6 +272,12 @@ 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 @@ -307,7 +313,6 @@ 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 36229c5..2e1703e 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -514,7 +514,15 @@ begin if (LPopup <> 0) and (LPopup <> LHwnd) and IsWindow(LPopup) then begin ShowWindow(LPopup, SW_SHOW); - SetForegroundWindow(LPopup); + // 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); + // 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', + 'A Save or folder dialog is waiting for an answer. Shortcuts stay ' + + 'disabled until you close it.', True); LogLine('Hotkey ignored — a native dialog is open; brought it to front.'); end; end;