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.
This commit is contained in:
+2
-1
@@ -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.
|
- **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).
|
- **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 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
|
### 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.
|
- **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.
|
||||||
|
|||||||
@@ -272,6 +272,12 @@ type
|
|||||||
read FOnQuickSearchHotkey write FOnQuickSearchHotkey;
|
read FOnQuickSearchHotkey write FOnQuickSearchHotkey;
|
||||||
end;
|
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
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
@@ -307,7 +313,6 @@ function UnhookWinEvent(hWinEventHook: THandle): BOOL;
|
|||||||
// Defined further down but needed by the constructor / MinimizeToTray.
|
// Defined further down but needed by the constructor / MinimizeToTray.
|
||||||
function IsOwnProcessWindow(AHwnd: HWND): Boolean; forward;
|
function IsOwnProcessWindow(AHwnd: HWND): Boolean; forward;
|
||||||
function IsShellWindow(AHwnd: HWND): Boolean; forward;
|
function IsShellWindow(AHwnd: HWND): Boolean; forward;
|
||||||
function ForceForegroundWindow(ATargetHwnd: HWND): Boolean; forward;
|
|
||||||
procedure ForegroundEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND;
|
procedure ForegroundEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND;
|
||||||
idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward;
|
idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward;
|
||||||
|
|
||||||
|
|||||||
@@ -514,7 +514,15 @@ begin
|
|||||||
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);
|
||||||
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.');
|
LogLine('Hotkey ignored — a native dialog is open; brought it to front.');
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
Reference in New Issue
Block a user