diff --git a/TEST_SCENARIOS.md b/TEST_SCENARIOS.md index 9e791d4..e48fd1f 100644 --- a/TEST_SCENARIOS.md +++ b/TEST_SCENARIOS.md @@ -308,6 +308,12 @@ l'éditeur slideover : créer + éditer une entrée, ajouter/retirer des tags, - **ACTION** : verrouiller → `Ctrl+Shift+T`. - **ATTENDU** : l'app revient au premier plan avec le focus sur le master password (même comportement que les autres hotkeys), pas de no-op silencieux. +### H7 · combo déjà pris par une autre application +- **ACTION** : assigner à un hotkey un combo qu'une autre app tient déjà en global (ex. `Ctrl+Shift+Q` si Quick Assist / un lanceur l'occupe ; sinon, tester en assignant le même combo dans deux apps). +- **ATTENDU** : toast **nommant le combo refusé** — « Already used by another app: 2FA code — pick another combo in Settings ». Les **autres** hotkeys restent fonctionnels (enregistrement best-effort, pas de tout-ou-rien). +- **ACTION 2** : fermer l'app concurrente → *Reset to defaults* (ou re-capturer le combo) → le hotkey redevient actif. +- **Historique** : le message disait « one or both hotkeys » (faux depuis qu'il y en a 3) sans dire lequel ; et un échec sur **Quick search** ou **New entry** n'était écrit que dans le panel debug — invisible en prod, le hotkey semblait juste cassé. + ### H6 · code en fin de période - **ACTION** : presser le hotkey juste avant la fin du compte à rebours de 30 s. - **ATTENDU** : le code tapé est celui de la période **courante** au moment de la frappe (généré à la demande, pas mis en cache). diff --git a/delphi-backend/Source/PM.Bridge.pas b/delphi-backend/Source/PM.Bridge.pas index 40bc648..21b7cc7 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -202,6 +202,12 @@ type property SecureClipboard: TSecureClipboard read FSecureClipboard; property TrayAdded: Boolean read FTrayAdded; property AutofillRegistered: Boolean read FAutofillRegistered; + // Per-slot outcome of the last SetAutofillHotkeys — lets the host name + // exactly which combo lost the race with another app's global hotkey + // instead of a vague "one of them failed". + property AutofillFullActive: Boolean read FAutofillFullActive; + property AutofillPwdActive: Boolean read FAutofillPwdActive; + property AutofillTotpActive: Boolean read FAutofillTotpActive; // Fired on main thread when Windows locks the session (WTS_SESSION_LOCK). property OnSystemLock: TProc read FOnSystemLock write FOnSystemLock; // True once WM_QUERYENDSESSION (or WM_ENDSESSION) has been received. diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index d870104..21ce2fa 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -863,6 +863,11 @@ begin // we register both with the supplied combos (replacing any prior). else if ACmd = 'autofill/hotkeys' then begin + // Names of the combos Windows refused (already held by another app), so + // the toast can say WHICH one is dead instead of leaving the user to + // wonder why a hotkey silently does nothing. A combo the user cleared + // (vk=0) is skipped on purpose and never listed here. + var LFailed := ''; // Quick-search combo is set unconditionally (independent of the // autofill enabled flag) so the picker stays armed even when the // autofill hotkeys are turned off. @@ -871,6 +876,7 @@ begin var LQsMods := Word(StrToIntDef(GetParam('qs_mods'), 6)); var LQsVk := Word(StrToIntDef(GetParam('qs_vk'), Ord('Q'))); var LQsOk := FBridge.SetQuickSearchHotkey(LQsMods, LQsVk); + if (not LQsOk) and (LQsVk <> 0) then LFailed := LFailed + 'Quick search, '; LogLine(Format('Quick-search hotkey set — mods:%d vk:%d (ok=%s)', [LQsMods, LQsVk, BoolToStr(LQsOk, True)])); end; @@ -881,6 +887,7 @@ begin var LNeMods := Word(StrToIntDef(GetParam('ne_mods'), 6)); var LNeVk := Word(StrToIntDef(GetParam('ne_vk'), Ord('A'))); var LNeOk := FBridge.SetNewEntryHotkey(LNeMods, LNeVk); + if (not LNeOk) and (LNeVk <> 0) then LFailed := LFailed + 'New entry, '; LogLine(Format('New-entry hotkey set — mods:%d vk:%d (ok=%s)', [LNeMods, LNeVk, BoolToStr(LNeOk, True)])); end; @@ -903,12 +910,19 @@ begin LTotpMods, LTotpVk); LogLine(Format('Autofill hotkeys set — full=mods:%d vk:%d pwd=mods:%d vk:%d totp=mods:%d vk:%d (all_ok=%s)', [LFullMods, LFullVk, LPwdMods, LPwdVk, LTotpMods, LTotpVk, BoolToStr(LAllOk, True)])); - // Notify JS of the result so the UI can flag a failed-to-register combo - // (typically a clash with another app's global hotkey). - WebBrowser.ExecuteJavaScript( - 'if(window.Bridge&&Bridge.onAutofillHotkeysResult)' + - 'Bridge.onAutofillHotkeysResult(' + BoolToStr(LAllOk, True).ToLower + ')'); + if (not FBridge.AutofillFullActive) and (LFullVk <> 0) then + LFailed := LFailed + 'Autofill (full), '; + if (not FBridge.AutofillPwdActive) and (LPwdVk <> 0) then + LFailed := LFailed + 'Password only, '; + if (not FBridge.AutofillTotpActive) and (LTotpVk <> 0) then + LFailed := LFailed + '2FA code, '; end; + // One notification for the whole batch, naming the combos that failed — + // empty string = everything registered. + LFailed := LFailed.TrimRight([',', ' ']); + WebBrowser.ExecuteJavaScript( + 'if(window.Bridge&&Bridge.onAutofillHotkeysResult)' + + 'Bridge.onAutofillHotkeysResult("' + LFailed.Replace('"', '') + '")'); end // execute: JS has matched an entry, decrypted the password, and is diff --git a/js/app.js b/js/app.js index b723650..e783021 100644 --- a/js/app.js +++ b/js/app.js @@ -218,12 +218,15 @@ const Bridge = (() => { // Called by Delphi after a setAutofillHotkeys request, with true if // BOTH combos registered successfully, false otherwise (clash with // another app holding a global hotkey). Surfaces a toast. - onAutofillHotkeysResult(allOk) { - if (allOk) { - toast('Autofill hotkeys updated'); - } else { - toast('Autofill: one or both hotkeys are already used by another app', 'warning'); - } + // `failed` = comma-separated names of the combos Windows refused + // (another app already owns them); empty string = all registered. + // Naming them matters: a clashing hotkey is otherwise indistinguishable + // from a broken feature — it just silently does nothing. + onAutofillHotkeysResult(failed) { + const list = String(failed || '').trim(); + if (!list) { toast('Hotkeys updated'); return; } + toast('Already used by another app: ' + list + + ' — pick another combo in Settings', 'warning'); }, // ---- Device-bound prefs (DPAPI-backed) ----------------------------