fix(hotkeys): never touch the window while a native modal owns it
Ctrl+Shift+Q and Ctrl+Shift+A call RestoreWindowFromTray before JS can refuse, so a JS guard is too late by construction. Restoring the owner of a Save As dialog stranded it behind everything and the app looked frozen while it was only disabled. NativeModalActive tests IsWindowEnabled - Windows disables the owner for the lifetime of the modal - and brings the dialog back to front via GetLastActivePopup instead of merely refusing.
This commit is contained in:
@@ -182,6 +182,9 @@ plus bas.
|
|||||||
- **ATTENDU** : le dialogue est toujours là, au premier plan de l'application, et répond. L'app n'est cliquable qu'à travers lui — c'est normal, une boîte native modale désactive sa fenêtre propriétaire tant qu'on n'a pas répondu.
|
- **ATTENDU** : le dialogue est toujours là, au premier plan de l'application, et répond. L'app n'est cliquable qu'à travers lui — c'est normal, une boîte native modale désactive sa fenêtre propriétaire tant qu'on n'a pas répondu.
|
||||||
- **Historique** : le refus « busy » appelait `cancelAutofill()`, dont le chemin Delphi fait un `SetForegroundWindow` vers la fenêtre capturée. Le dialogue passait **derrière** l'autre application, et l'app paraissait figée : bloquée par une modale devenue introuvable. `refocus=0` distingue désormais **refuser** une demande (ne rien déplacer) de **terminer** une interaction utilisateur (rendre le focus à la cible).
|
- **Historique** : le refus « busy » appelait `cancelAutofill()`, dont le chemin Delphi fait un `SetForegroundWindow` vers la fenêtre capturée. Le dialogue passait **derrière** l'autre application, et l'app paraissait figée : bloquée par une modale devenue introuvable. `refocus=0` distingue désormais **refuser** une demande (ne rien déplacer) de **terminer** une interaction utilisateur (rendre le focus à la cible).
|
||||||
- **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).
|
||||||
|
- **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.
|
||||||
|
|
||||||
### 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.
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ type
|
|||||||
// stuck raised (see the implementation).
|
// stuck raised (see the implementation).
|
||||||
// One-shot: hides the window the first time FMX shows it on a --tray
|
// One-shot: hides the window the first time FMX shows it on a --tray
|
||||||
// launch, then unhooks itself.
|
// launch, then unhooks itself.
|
||||||
|
// True when a native modal dialog owns us — see the implementation.
|
||||||
|
function NativeModalActive: Boolean;
|
||||||
procedure RestoreWindowFromTray;
|
procedure RestoreWindowFromTray;
|
||||||
procedure WinStateMinimizeStart;
|
procedure WinStateMinimizeStart;
|
||||||
procedure WinStateMinimizeEnd;
|
procedure WinStateMinimizeEnd;
|
||||||
@@ -470,6 +472,7 @@ begin
|
|||||||
// cmd://autofill/execute (fired after the user picks an entry) sends
|
// cmd://autofill/execute (fired after the user picks an entry) sends
|
||||||
// the password into it. Then pop the modal in fill mode.
|
// the password into it. Then pop the modal in fill mode.
|
||||||
if not FServer.Active then Exit;
|
if not FServer.Active then Exit;
|
||||||
|
if NativeModalActive then Exit;
|
||||||
FAutofillTargetHWND := ATargetHWND;
|
FAutofillTargetHWND := ATargetHWND;
|
||||||
// Tray-hidden ONLY (Visible=false). A window merely minimised to the
|
// Tray-hidden ONLY (Visible=false). A window merely minimised to the
|
||||||
// taskbar must NOT count: hide-after would send it to the TRAY on close —
|
// taskbar must NOT count: hide-after would send it to the TRAY on close —
|
||||||
@@ -494,6 +497,28 @@ begin
|
|||||||
[IntToHex(ATargetHWND, 8), ATitle]));
|
[IntToHex(ATargetHWND, 8), ATitle]));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// A native modal dialog (Save As, folder picker) DISABLES its owner window for
|
||||||
|
// as long as it is up. Restoring / focusing that owner then strands the dialog
|
||||||
|
// behind everything: the app looks frozen, when it is merely disabled by a modal
|
||||||
|
// 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
|
||||||
|
// actually needs.
|
||||||
|
function TMainForm.NativeModalActive: Boolean;
|
||||||
|
var
|
||||||
|
LHwnd, LPopup: HWND;
|
||||||
|
begin
|
||||||
|
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
||||||
|
Result := (LHwnd <> 0) and (not IsWindowEnabled(LHwnd));
|
||||||
|
if not Result then Exit;
|
||||||
|
LPopup := GetLastActivePopup(LHwnd);
|
||||||
|
if (LPopup <> 0) and (LPopup <> LHwnd) and IsWindow(LPopup) then
|
||||||
|
begin
|
||||||
|
ShowWindow(LPopup, SW_SHOW);
|
||||||
|
SetForegroundWindow(LPopup);
|
||||||
|
LogLine('Hotkey ignored — a native dialog is open; brought it to front.');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.BridgeQuickSearchRequest;
|
procedure TMainForm.BridgeQuickSearchRequest;
|
||||||
var
|
var
|
||||||
LWasHidden: Boolean;
|
LWasHidden: Boolean;
|
||||||
@@ -503,6 +528,7 @@ begin
|
|||||||
// see the modal, then ask JS to open it. JS handles the locked-vault
|
// see the modal, then ask JS to open it. JS handles the locked-vault
|
||||||
// case (shows the auth screen with master-pw focused instead).
|
// case (shows the auth screen with master-pw focused instead).
|
||||||
if not FServer.Active then Exit;
|
if not FServer.Active then Exit;
|
||||||
|
if NativeModalActive then Exit;
|
||||||
// Remember whether the window was TRAY-hidden BEFORE we restore — after
|
// Remember whether the window was TRAY-hidden BEFORE we restore — after
|
||||||
// the user picks an entry the JS layer asks us to minimize back so they
|
// the user picks an entry the JS layer asks us to minimize back so they
|
||||||
// can paste into the target app without an extra alt-tab. Iconic-only
|
// can paste into the target app without an extra alt-tab. Iconic-only
|
||||||
@@ -522,6 +548,7 @@ var
|
|||||||
EscapedTitle: string;
|
EscapedTitle: string;
|
||||||
begin
|
begin
|
||||||
if not FServer.Active then Exit;
|
if not FServer.Active then Exit;
|
||||||
|
if NativeModalActive then Exit;
|
||||||
RestoreWindowFromTray;
|
RestoreWindowFromTray;
|
||||||
EscapedTitle := StringReplace(AWindowTitle, '\', '\\', [rfReplaceAll]);
|
EscapedTitle := StringReplace(AWindowTitle, '\', '\\', [rfReplaceAll]);
|
||||||
EscapedTitle := StringReplace(EscapedTitle, '"', '\"', [rfReplaceAll]);
|
EscapedTitle := StringReplace(EscapedTitle, '"', '\"', [rfReplaceAll]);
|
||||||
@@ -1804,6 +1831,7 @@ var
|
|||||||
LTitle, LKind: string;
|
LTitle, LKind: string;
|
||||||
begin
|
begin
|
||||||
if not FServer.Active then Exit;
|
if not FServer.Active then Exit;
|
||||||
|
if NativeModalActive then Exit;
|
||||||
FAutofillTargetHWND := ATargetHWND;
|
FAutofillTargetHWND := ATargetHWND;
|
||||||
// Tray-hidden only, like the quick-search path: a window merely minimised to
|
// Tray-hidden only, like the quick-search path: a window merely minimised to
|
||||||
// the taskbar must not come back as a tray icon it never was.
|
// the taskbar must not come back as a tray icon it never was.
|
||||||
|
|||||||
Reference in New Issue
Block a user