diff --git a/TEST_SCENARIOS.md b/TEST_SCENARIOS.md index e1154b8..d94f77f 100644 --- a/TEST_SCENARIOS.md +++ b/TEST_SCENARIOS.md @@ -175,7 +175,13 @@ plus bas. - **Pourquoi les hotkeys échappent aux gardes UI** : ils arrivent de Win32, **hors de la fenêtre**. L'overlay qui couvre l'application n'intercepte rien : le panneau s'ouvrait derrière lui, inaccessible. - **Où est posé le garde** : dans `openSlideOver` et `openQuickSearchModal`, pas sur chaque déclencheur — c'est le point de passage de *tous* les appelants (hotkey, palette de commandes, bouton *Fix* du vault health). Sûr parce qu'**aucun chemin de sauvegarde n'affiche l'overlay** : rien de légitime n'ouvre l'éditeur pendant une opération bloquante. - **ACTION 2 — autofill** : pendant l'overlay, `Ctrl+Shift+L`, `Ctrl+Shift+P`, `Ctrl+Shift+T`, avec un titre à **un seul** candidat puis à **plusieurs**. -- **ATTENDU** : rien ne se remplit, aucun picker, même toast. Le garde est dans `autofillHandleRequest`, en amont du choix match-unique / picker, donc il couvre les trois hotkeys et les deux issues. Il appelle `Bridge.cancelAutofill()` pour que Delphi ne garde pas la fenêtre cible capturée en attente d'un remplissage qui ne viendra jamais. +- **ATTENDU** : rien ne se remplit, aucun picker, même toast. Le garde est dans `autofillHandleRequest`, en amont du choix match-unique / picker, donc il couvre les trois hotkeys et les deux issues. Il appelle `Bridge.cancelAutofill(false)` pour que Delphi ne garde pas la fenêtre cible capturée en attente d'un remplissage qui ne viendra jamais. + +### B17 · hotkeys pendant la boîte « Enregistrer sous » de l'export +- **ACTION** : lancer un *Export* → à l'apparition du dialogue natif **Enregistrer sous**, basculer sur une autre application (VS Code) → presser les hotkeys d'autofill → revenir à l'app. +- **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). +- **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. ### 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/UMainForm.pas b/delphi-backend/UMainForm.pas index 10afc57..9aa9267 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -1093,13 +1093,18 @@ begin // foreground process at that moment, so plain SetForegroundWindow works. else if ACmd = 'autofill/cancel' then begin - if FAutofillTargetHWND <> 0 then + // refocus=0 — JS is REFUSING the request, not ending a user interaction: + // nothing must move on screen. During an export a native Save dialog owns + // the foreground, and pushing it behind leaves the app disabled by a modal + // the user can no longer find. + var LRefocus := GetParam('refocus') <> '0'; + if LRefocus and (FAutofillTargetHWND <> 0) then SetForegroundWindow(FAutofillTargetHWND); FAutofillTargetHWND := 0; // The window only came out of the tray to show the picker, so dismissing it // puts it back — same as quick-search, which hides on Esc too. Done after // handing focus to the target above. - if FAutofillHotkeyWasHidden then + if FAutofillHotkeyWasHidden and LRefocus then begin FAutofillHotkeyWasHidden := False; // Foreground already handed to the target above. diff --git a/js/app.autofill.js b/js/app.autofill.js index a0cc669..06f2011 100644 --- a/js/app.autofill.js +++ b/js/app.autofill.js @@ -242,7 +242,10 @@ async function autofillHandleRequest(windowTitle, kind) { // window and hand the foreground away mid-operation. cancelAutofill so // Delphi doesn't keep the captured target HWND for a fill that never comes. if (typeof isBusy === 'function' && isBusy()) { - Bridge.cancelAutofill(); + // refocus=false: refusing must not move any window. During an export a + // native Save dialog holds the foreground, and shoving it behind leaves + // the app disabled by a modal the user can no longer find. + Bridge.cancelAutofill(false); toast('Busy — try again in a moment', 'warning'); return; } diff --git a/js/app.js b/js/app.js index dcaa4eb..dfbd7d6 100644 --- a/js/app.js +++ b/js/app.js @@ -177,10 +177,14 @@ const Bridge = (() => { cmd('cmd://app/theme?mode=' + (mode === 'light' ? 'light' : 'dark')); }, - // Tell Delphi we couldn't find a match / user cancelled. - cancelAutofill() { + // Tell Delphi we couldn't find a match / user cancelled. It normally + // hands the foreground back to the captured target — pass false when + // merely REFUSING a request, so nothing on screen moves. A native Save + // dialog owns the foreground during an export, and pushing it behind + // leaves the app disabled by a modal the user can no longer see. + cancelAutofill(refocus = true) { if (!active) return; - cmd('cmd://autofill/cancel'); + cmd('cmd://autofill/cancel' + (refocus ? '' : '?refocus=0')); }, // Sync the hotkey registration state with this device's preference.