fix(hotkeys): find the native dialog by enumerating the thread windows
GetLastActivePopup returns the form itself - the common Save dialog is not an owned popup - so the code that raises it never ran. It does live on our thread with the standard #32770 class, which EnumThreadWindows finds regardless of its owner. Logging confirmed two things worth keeping: WM_HOTKEY is dispatched during the dialog modal loop, and IsWindowEnabled detects the state correctly.
This commit is contained in:
+3
-1
@@ -185,7 +185,9 @@ 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).
|
- **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.
|
- **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.
|
- **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.
|
||||||
- **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 n°1, localiser le dialogue** : `GetLastActivePopup` renvoie **le formulaire lui-même** — la boîte « Enregistrer sous » n'est pas une popup possédée par notre fenêtre. Elle vit en revanche sur notre thread avec la classe standard `#32770`, donc `EnumThreadWindows` la trouve quel que soit son propriétaire. Mesuré : sans ça le bloc de remontée ne s'exécutait jamais, silencieusement.
|
||||||
|
- **Piège n°2, le 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.
|
||||||
|
- **Vérifié au log** : le `WM_HOTKEY` **est** bien distribué pendant la boucle modale du dialogue, et `IsWindowEnabled` détecte correctement l'état. Ces deux points étaient les hypothèses coûteuses à écarter avant de chercher ailleurs.
|
||||||
- **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.
|
- **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
|
### B14 · dialogues natifs ouverts par un clic programmatique
|
||||||
|
|||||||
@@ -201,6 +201,11 @@ implementation
|
|||||||
|
|
||||||
{$R *.fmx}
|
{$R *.fmx}
|
||||||
|
|
||||||
|
// Result slot for EnumOwnDialogProc — the enumeration is main-thread only and
|
||||||
|
// consumed immediately, so a unit variable is enough.
|
||||||
|
var
|
||||||
|
GFoundDialogHwnd: HWND = 0;
|
||||||
|
|
||||||
// Forward — used by WebBrowserInitialized (which sits above the actual
|
// Forward — used by WebBrowserInitialized (which sits above the actual
|
||||||
// definition lower in the unit).
|
// definition lower in the unit).
|
||||||
function MaskAccessToken(const AUrl: string): string; forward;
|
function MaskAccessToken(const AUrl: string): string; forward;
|
||||||
@@ -503,6 +508,24 @@ end;
|
|||||||
// the user can no longer find. Every hotkey entry point checks this first — and
|
// the user can no longer find. Every hotkey entry point checks this first — and
|
||||||
// rather than just refusing, it surfaces the dialog, which is what the user
|
// rather than just refusing, it surfaces the dialog, which is what the user
|
||||||
// actually needs.
|
// actually needs.
|
||||||
|
// The common Save/Open dialog is NOT an owned popup of the form —
|
||||||
|
// GetLastActivePopup returns the form itself — but it does live on our thread
|
||||||
|
// and carries the standard dialog class. Enumerating the thread's windows finds
|
||||||
|
// it whoever its owner turns out to be.
|
||||||
|
function EnumOwnDialogProc(AWnd: HWND; AParam: LPARAM): BOOL; stdcall;
|
||||||
|
var
|
||||||
|
LCls: array[0..63] of Char;
|
||||||
|
begin
|
||||||
|
Result := True; // keep enumerating
|
||||||
|
if (AWnd = HWND(AParam)) or (not IsWindowVisible(AWnd)) then Exit;
|
||||||
|
if GetClassName(AWnd, LCls, Length(LCls)) = 0 then Exit;
|
||||||
|
if SameText(LCls, '#32770') then
|
||||||
|
begin
|
||||||
|
GFoundDialogHwnd := AWnd;
|
||||||
|
Result := False; // stop
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
function TMainForm.NativeModalActive: Boolean;
|
function TMainForm.NativeModalActive: Boolean;
|
||||||
var
|
var
|
||||||
LHwnd, LPopup: HWND;
|
LHwnd, LPopup: HWND;
|
||||||
@@ -510,7 +533,10 @@ begin
|
|||||||
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
||||||
Result := (LHwnd <> 0) and (not IsWindowEnabled(LHwnd));
|
Result := (LHwnd <> 0) and (not IsWindowEnabled(LHwnd));
|
||||||
if not Result then Exit;
|
if not Result then Exit;
|
||||||
LPopup := GetLastActivePopup(LHwnd);
|
GFoundDialogHwnd := 0;
|
||||||
|
EnumThreadWindows(GetCurrentThreadId, @EnumOwnDialogProc, LPARAM(LHwnd));
|
||||||
|
LPopup := GFoundDialogHwnd;
|
||||||
|
if LPopup = 0 then LPopup := GetLastActivePopup(LHwnd); // fallback
|
||||||
if (LPopup <> 0) and (LPopup <> LHwnd) and IsWindow(LPopup) then
|
if (LPopup <> 0) and (LPopup <> LHwnd) and IsWindow(LPopup) then
|
||||||
begin
|
begin
|
||||||
ShowWindow(LPopup, SW_SHOW);
|
ShowWindow(LPopup, SW_SHOW);
|
||||||
|
|||||||
Reference in New Issue
Block a user